diff --git a/Package.swift b/Package.swift index a8175e8c..c9ac0c48 100644 --- a/Package.swift +++ b/Package.swift @@ -7,7 +7,7 @@ let package = Package( name: "swift-graphql", platforms: [ .iOS(.v13), - .macOS(.v10_15), + .macOS(.v11), .tvOS(.v13), .watchOS(.v6) ], diff --git a/Sources/GraphQLAST/Introspection.swift b/Sources/GraphQLAST/Introspection.swift index 7b7ed7da..f274f87a 100644 --- a/Sources/GraphQLAST/Introspection.swift +++ b/Sources/GraphQLAST/Introspection.swift @@ -225,6 +225,10 @@ public extension Schema { /// Downloads a schema from the provided endpoint. init(from endpoint: URL, withHeaders headers: [String: String] = [:]) throws { let introspection: Data = try fetch(from: endpoint, withHeaders: headers) + try self.init(introspection: introspection) + } + + init(introspection: Data) throws { self = try parse(introspection) } } diff --git a/Sources/SwiftGraphQL/HTTP+WebSockets.swift b/Sources/SwiftGraphQL/HTTP+WebSockets.swift index 5d09dcd4..32a21d27 100644 --- a/Sources/SwiftGraphQL/HTTP+WebSockets.swift +++ b/Sources/SwiftGraphQL/HTTP+WebSockets.swift @@ -1,13 +1,29 @@ import Foundation +import Network + +import os.log + +extension OSLog { + private static var subsystem = Bundle.main.bundleIdentifier! + static let subscription = OSLog(subsystem: subsystem, category: "subscription") +} + +public typealias URLSessionGraphQLSocket = GraphQLSocket +public typealias NWConnectionGraphQLSocket = GraphQLSocket public protocol GraphQLEnabledSocket { associatedtype InitParamaters - associatedtype New: GraphQLEnabledSocket where New == Self - static func create(with params: InitParamaters) -> New + associatedtype New where New == Self + static func create(with params: InitParamaters, errorHandler: @escaping (GraphQLSocket.SubscribeError) -> Void) -> New /// - parameter errorHandler: A closure that receives an Error that indicates an error encountered while sending. func send(message: Data, errorHandler: @escaping (Error) -> Void) - func receiveMessages(_ handler: @escaping (Result) -> Void) + /// - returns: On true, will stop listening to the socket + func receiveMessages(_ handler: @escaping (Result) -> Bool) +} + +final class SinglePingQueueToken { + var cancelled = false } /// https://github.com/enisdenjo/graphql-ws/blob/master/PROTOCOL.md @@ -19,9 +35,11 @@ public class GraphQLSocket { case notRunning, started, running } + private let restartQueue = DispatchQueue(label: "GraphQLSocketRestartQueue") private var socket: S? private var initParams: S.InitParamaters private var autoConnect: Bool + private var pingInterval: TimeInterval? private var lastConnectionParams = AnyCodable([String: String]()) private var state: SocketState = .notRunning { didSet { startQueue() } @@ -32,9 +50,16 @@ public class GraphQLSocket { private var decoder = JSONDecoder() private var encoder = JSONEncoder() - public init(_ params: S.InitParamaters, autoConnect: Bool = false) { + // Every successful ping should be matched by a successful pong + private var pingPongMatches: Int = 0 + + // we should never have more than one ping queue token + var pingQueueToken: SinglePingQueueToken? + + public init(_ params: S.InitParamaters, autoConnect: Bool = false, pingInterval: TimeInterval? = nil) { self.initParams = params self.autoConnect = autoConnect + self.pingInterval = pingInterval } public enum StartError: Error { @@ -44,50 +69,132 @@ public class GraphQLSocket { } /// Starts a socket without connectionParams. - public func start(errorHandler: @escaping (StartError) -> Void) { + public func start(errorHandler: @escaping (SubscribeError) -> Void) { start(connectionParams: [String: String](), errorHandler: errorHandler) } /// Starts a socket. - public func start

(connectionParams: P, errorHandler: @escaping (StartError) -> Void) { + public func start

(connectionParams: P, errorHandler: @escaping (SubscribeError) -> Void) { guard state == .notRunning else { - return errorHandler(.alreadyStarted) + return errorHandler(.startError(.alreadyStarted)) } do { lastConnectionParams = AnyCodable(connectionParams) let message = Message.connectionInit(connectionParams) let messageData = try encoder.encode(message) - print(try! JSONSerialization.jsonObject(with: messageData)) +// os_log("Start Connection: %{public}@", +// log: OSLog.subscription, +// type: .debug, +// (String(data: messageData, encoding: .utf8) ?? "Invalid .utf8") +// ) state = .started - socket = S.create(with: initParams) + socket = S.create(with: initParams, errorHandler: errorHandler) socket?.send(message: messageData, errorHandler: { [weak self] in - self?.stop() - errorHandler(.connectionInit(error: $0)) + self?.restart(errorHandler: errorHandler) + errorHandler(.startError(.connectionInit(error: $0))) }) - socket?.receiveMessages { [weak self] message in + socket?.receiveMessages { [weak self] (message) -> Bool in switch message { case .success(let data): - let message = try! JSONDecoder().decode(Message.self, from: data) +// os_log("Received Data: %{public}@", +// log: OSLog.subscription, +// type: .debug, (String(data: data, encoding: .utf8) ?? "Invalid .utf8") +// ) + guard var message = try? JSONDecoder().decode(Message.self, from: data) else { + os_log("Invalid JSON Payload", log: OSLog.subscription, type: .debug) + return false + } switch message.type { case .connection_ack: self?.state = .running - case .next, .error, .complete: - guard let id = message.id else { return } + // If we have a time interval, set up a ping thread + if let pingInterval = self?.pingInterval { + // cancel the old token + self?.pingQueueToken?.cancelled = true + let token = SinglePingQueueToken() + self?.pingQueueToken = token + self?.detachedPingQueue(interval: pingInterval, errorHandler: { error in + errorHandler(.pingFailed(error)) + }, pingHandler: { [weak self] in + guard let self = self else { return } + self.pingPongMatches += 1 + // If we pinged 5 times without a pong, try to restart + if self.pingPongMatches > 5 { + self.pingPongMatches = 0 + self.restart(errorHandler: errorHandler) + } + }, token: token) + } + case .ka: + self?.state = .running + case .next, .error, .complete, .data: + guard let id = message.id else { return false } + message.originalData = data self?.subscriptions[id]?(message) - case .subscribe, .connection_init: + case .connection_terminate, .connection_error: + self?.restart(errorHandler: errorHandler) + return true + case .pong: + self?.pingPongMatches -= 1 + self?.state = .running + case .subscribe, .connection_init, .ping: _ = "The server will never send these messages" } - case .failure(_): + case .failure(let failure): + os_log("Received Error: %{public}@", log: OSLog.subscription, type: .debug, failure.localizedDescription) + // Retry the start in a couple of seconds. // Should we send this error to the start errorHandler? // This could happen during the entire lifetime of the socket so // it's not really a start error - self?.stop() + //errorHandler(.startError(.connectionInit(error: failure))) + self?.restart(errorHandler: errorHandler) + return true } + return false } } catch { - return errorHandler(.failedToEncodeConnectionParams(error: error)) + return errorHandler(.startError(.failedToEncodeConnectionParams(error: error))) + } + } + + private func detachedPingQueue( + interval: TimeInterval, + errorHandler: @escaping (Error) -> Void, + pingHandler: @escaping () -> Void, + token: SinglePingQueueToken + ) { + // if there is a new token, leave here, so there's always only one ping queue + guard !token.cancelled else { + return + } + if state == .notRunning { + // Try again while we're restarting + restartQueue.asyncAfter(deadline: .now() + 3.0) { [weak self] in + self?.detachedPingQueue(interval: interval, errorHandler: errorHandler, pingHandler: pingHandler, token: token) + } + return + } + restartQueue.asyncAfter(deadline: .now() + interval) { [weak self] in + guard let self = self else { + return + } + do { + let message = Message.ping() + let messageData = try self.encoder.encode(message) + self.socket?.send(message: messageData, errorHandler: errorHandler) + pingHandler() + os_log( + "Ping", + log: OSLog.subscription, + type: .debug + ) + } catch let error { + errorHandler(error) + } + // Schedule the next + self.detachedPingQueue(interval: interval, errorHandler: errorHandler, pingHandler: pingHandler, token: token) } } @@ -99,7 +206,9 @@ public class GraphQLSocket { case failedToDecodeGraphQLErrors(Error) case errors([GraphQLError]) case subscribeFailed(Error) + case pingFailed(Error) case complete + case startError(StartError) } public func subscribe( @@ -112,19 +221,42 @@ public class GraphQLSocket { self?.complete(id: id) } + #if DEBUG + #if targetEnvironment(simulator) + let payload = selection.buildPayload(operationName: operationName) + + // Write the query. We also need the id, so we encode it into the variables + try? payload.query.write(toFile: "/tmp/query_\(id).graphql", atomically: true, encoding: .utf8) + // Write the variables + var copiedVariables = payload.variables + copiedVariables["gql-subscription-id"] = AnyCodable(stringLiteral: id) + if let variables = try? encoder.encode(copiedVariables) { + try? variables.write(to: URL(fileURLWithPath: "/tmp/query_variables_\(id).json")) + } + #endif + #endif + switch state { case .notRunning: if autoConnect { queue += [{ [weak cancellable] in cancellable?.add($0.subscribe(to: selection, operationName: operationName, eventHandler: eventHandler)) }] - start(connectionParams: lastConnectionParams, errorHandler: { print($0) }) + start(connectionParams: lastConnectionParams, errorHandler: { + eventHandler(.failure($0)) + }) } else { - print("GraphQLSocket: Call start first or enable autoConnect") + os_log("GraphQLSocket: Call start first or enable autoConnect", + log: OSLog.subscription, + type: .debug + ) eventHandler(.failure(.notStartedAndNoAutoConnect)) } case .started: - print("GraphQLSocket: Still waiting for connection_ack from the server so subscribe is queued") + os_log("GraphQLSocket: Still waiting for connection_ack from the server so subscribe is queued", + log: OSLog.subscription, + type: .debug + ) queue += [{ [weak cancellable] in cancellable?.add($0.subscribe(to: selection, operationName: operationName, eventHandler: eventHandler)) }] @@ -133,29 +265,48 @@ public class GraphQLSocket { let payload = selection.buildPayload(operationName: operationName) let message = Message.subscribe(payload, id: id) let messageData = try encoder.encode(message) +// os_log("Outgoing Data: %{public}@", +// log: OSLog.subscription, +// type: .debug, (String(data: messageData, encoding: .utf8) ?? "Invalid .utf8") +// ) socket?.send(message: messageData, errorHandler: { eventHandler(.failure(.subscribeFailed($0))) }) subscriptions[id] = { message in switch message.type { - case .next: + case .next, .data: do { + if let originalData = message.originalData { + +#if DEBUG +#if targetEnvironment(simulator) + // write the response out. A given subscription can have multiple responses. + let debugTime = DispatchTime.now().uptimeNanoseconds + let url = URL(fileURLWithPath: "/tmp/subscription_response_\(id)_\(debugTime).json") + try? originalData.write(to: url) +#endif +#endif + } let result = try GraphQLResult(webSocketMessage: message, with: selection) eventHandler(.success(result)) } catch { eventHandler(.failure(.failedToDecodeSelection(error))) } - case .error: + case .error, .connection_error: do { let result: [GraphQLError] = try message.decodePayload() eventHandler(.failure(.errors(result))) } catch { eventHandler(.failure(.failedToDecodeGraphQLErrors(error))) } + case .connection_terminate: + eventHandler(.failure(.complete)) case .complete: eventHandler(.failure(.complete)) - case .connection_init, .connection_ack, .subscribe: - fatalError() + case .ka, .pong: () + case .connection_init, .connection_ack, .subscribe, .ping: + os_log("Invalid subscription case %{public}@", log: OSLog.subscription, type: .debug, message.type.rawValue) + assertionFailure() } } @@ -173,6 +324,15 @@ public class GraphQLSocket { socket = nil } + /// try to restart the socket after a brief delay + public func restart(errorHandler: @escaping (SubscribeError) -> Void) { + self.state = .notRunning + let params = lastConnectionParams + restartQueue.asyncAfter(deadline: .now() + 3.0) { [weak self] in + self?.start(connectionParams: params, errorHandler: errorHandler) + } + } + private func complete(id: String) { subscriptions[id] = nil let message = Message.complete(id: id) @@ -198,8 +358,16 @@ public struct GraphQLSocketMessage: Codable { case next case error case complete + case ka + case connection_error + case connection_terminate + case data + case ping + case pong } + public var originalData: Data? + public var type: MessageType public var id: String? /// Used for retreiving payload after decoding incomming message @@ -250,6 +418,10 @@ extension GraphQLSocketMessage { return .init(type: .connection_init, id: nil, addedPayload: AnyCodable(connectionParams)) } + public static func ping() -> GraphQLSocketMessage { + return .init(type: .ping, id: nil, addedPayload: AnyCodable([:])) + } + /// Requests an operation specified in the message `payload`. This message provides a /// unique ID field to connect published messages to the operation requested by this message. public static func subscribe

(_ payload: P, id: String) -> GraphQLSocketMessage { @@ -340,6 +512,108 @@ extension SocketCancellable { } #endif +@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) +extension NWConnection: GraphQLEnabledSocket { + public struct InitParamaters { + let url: URL + let headers: HttpHeaders + let queue: DispatchQueue + + public init(url: URL, headers: HttpHeaders, queue: DispatchQueue) { + self.url = url + self.headers = headers + self.queue = queue + } + } + + public typealias New = NWConnection + + public class func create(with params: InitParamaters, errorHandler: @escaping (GraphQLSocket.SubscribeError) -> Void) -> NWConnection { + + let endpoint = NWEndpoint.url(params.url) + let parameters: NWParameters = params.url.scheme == "wss" ? .tls : .tcp + let websocketOptions = NWProtocolWebSocket.Options() + websocketOptions.autoReplyPing = true + websocketOptions.maximumMessageSize = 1024 * 1024 * 100 + + var headers: [(String, String)] = [] + for header in params.headers { + headers.append((header.key, header.value)) + } + headers.append(("Sec-WebSocket-Protocol", "graphql-transport-ws")) + headers.append(("Content-Type", "application/json")) + websocketOptions.setAdditionalHeaders(headers) + + parameters.defaultProtocolStack.applicationProtocols.insert( + websocketOptions, + at: 0 + ) + let connection = NWConnection(to: endpoint, using: parameters) + + connection.stateUpdateHandler = { state in + switch state { + case .ready: + os_log("Connection Ready", log: OSLog.subscription, type: .debug) + case .failed(let error): + os_log("Connection Failed: %{public}@", + log: OSLog.subscription, + type: .error, + error.localizedDescription + ) + errorHandler(.subscribeFailed(error)) + case .waiting(let error): + os_log("Waiting Error: %{public}@", + log: OSLog.subscription, + type: .error, + error.localizedDescription + ) + errorHandler(.subscribeFailed(error)) + + case .setup: +// os_log("Setup State Update", log: OSLog.subscription, type: .debug) + () + case .preparing: +// os_log("Preparing State Update", log: OSLog.subscription, type: .debug) + () + case .cancelled: + os_log("Cancelled State Update", log: OSLog.subscription, type: .debug) + @unknown default: + os_log("Unknown State Update", log: OSLog.subscription, type: .debug) + } + } + + connection.start(queue: params.queue) + return connection + } + + public func send(message: Data, errorHandler: @escaping (Error) -> Void) { + self.send(content: message, completion: .contentProcessed({ error in + guard let error = error else { return } + errorHandler(error) + })) + } + + public func receiveMessages(_ handler: @escaping (Result) -> Bool) { + // Create an event handler. + func receiveNext(on socket: NWConnection?) { + socket?.receiveMessage(completion: { completeContent, contentContext, isComplete, error in + let cancel: Bool + switch (completeContent, error) { + case (let content?, _): + cancel = handler(.success(content)) + case (_, let error?): + cancel = handler(.failure(error)) + default: + cancel = false + } + guard cancel == false else { return } + receiveNext(on: socket) + }) + } + + receiveNext(on: self) + } +} @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) extension URLSessionWebSocketTask: GraphQLEnabledSocket { @@ -356,7 +630,7 @@ extension URLSessionWebSocketTask: GraphQLEnabledSocket { } public typealias New = URLSessionWebSocketTask - public class func create(with params: InitParamaters) -> URLSessionWebSocketTask { + public class func create(with params: InitParamaters, errorHandler: @escaping (GraphQLSocket.SubscribeError) -> Void) -> URLSessionWebSocketTask { var request = URLRequest(url: params.url) for header in params.headers { request.setValue(header.value, forHTTPHeaderField: header.key) @@ -370,6 +644,12 @@ extension URLSessionWebSocketTask: GraphQLEnabledSocket { } public func send(message: Data, errorHandler: @escaping (Error) -> Void) { +// os_log( +// "Send data: %{private}@", +// log: OSLog.subscription, +// type: .debug, +// String(data: message, encoding: .utf8) ?? "Invalid Encoding" +// ) self.send(.data(message), completionHandler: { if let error = $0 { errorHandler(error) @@ -377,12 +657,15 @@ extension URLSessionWebSocketTask: GraphQLEnabledSocket { }) } - public func receiveMessages(_ handler: @escaping (Result) -> Void) { - print("receiveMessages") + public func receiveMessages(_ handler: @escaping (Result) -> Bool) { // Create an event handler. func receiveNext(on socket: URLSessionWebSocketTask?) { socket?.receive { [weak socket] result in - handler(result.map(\.data)) + let cancel = handler(result.map(\.data)) + guard cancel == false else { + socket?.cancel(with: .goingAway, reason: nil) + return + } receiveNext(on: socket) } } diff --git a/Sources/SwiftGraphQL/HTTP.swift b/Sources/SwiftGraphQL/HTTP.swift index c216fb81..51f82372 100644 --- a/Sources/SwiftGraphQL/HTTP.swift +++ b/Sources/SwiftGraphQL/HTTP.swift @@ -89,32 +89,62 @@ private func send( return nil } + let debugTime = DispatchTime.now().uptimeNanoseconds + // Construct a GraphQL request. let request = createGraphQLRequest( selection: selection, operationName: operationName, url: url, headers: headers, - method: method + method: method, + debugTime: debugTime ) // Create a completion handler. func onComplete(data: Data?, response: URLResponse?, error: Error?) { + + // Save the response or the error, depending on what's available + #if DEBUG + #if targetEnvironment(simulator) + let fallback = "\(String(describing: response))".data(using: .utf8) ?? "{'error': 'Could not serialize response'}".data(using: .utf8)! + let responeData: Data + if let data = data { + responeData = data + } else if let error = error { + responeData = "{'error': '\(error.localizedDescription)'}".data(using: .utf8) + ?? fallback + } else { + responeData = fallback + } + let url = URL(fileURLWithPath: "/tmp/query_response_\(debugTime).json") + try? responeData.write(to: url) + #endif + #endif + /* Process the response. */ // Check for HTTP errors. if let error = error { return completionHandler(.failure(.network(error))) } - guard let httpResponse = response as? HTTPURLResponse, - (200 ... 299).contains(httpResponse.statusCode) - else { - return completionHandler(.failure(.badstatus)) + guard let httpResponse = response as? HTTPURLResponse else { + return completionHandler(.failure(.badstatus(nil))) + } + guard (200 ... 299).contains(httpResponse.statusCode) else { + return completionHandler(.failure(.badstatus(httpResponse.statusCode))) } // Try to serialize the response. - if let data = data, let result = try? GraphQLResult(data, with: selection) { - return completionHandler(.success(result)) + if let data = data { + do { + let result = try GraphQLResult(data, with: selection) + return completionHandler(.success(result)) + } catch let error as HttpError { + return completionHandler(.failure(error)) + } catch let error { + return completionHandler(.failure(.decodingError(error, extensions: nil))) + } } return completionHandler(.failure(.badpayload)) @@ -137,18 +167,24 @@ public enum HttpError: Error { case timeout case network(Error) case badpayload - case badstatus + case badstatus(Int?) case cancelled + case decodingError(Error, extensions: [String: AnyCodable]?) + case graphQLErrors([GraphQLError], extensions: [String: AnyCodable]?) } extension HttpError: Equatable { public static func == (lhs: SwiftGraphQL.HttpError, rhs: SwiftGraphQL.HttpError) -> Bool { // Equals if they are of the same type, different otherwise. switch (lhs, rhs) { + case (.badstatus(let a), .badstatus(let b)): return a == b case (.badURL, badURL), - (.timeout, .timeout), - (.badpayload, .badpayload), - (.badstatus, .badstatus): + (.timeout, .timeout), + (.badpayload, .badpayload), + (.cancelled, .cancelled), + (.network, .network), + (.decodingError, .decodingError), + (.graphQLErrors, .graphQLErrors): return true default: return false @@ -183,7 +219,8 @@ private func createGraphQLRequest( operationName: String?, url: URL, headers: HttpHeaders, - method: HttpMethod + method: HttpMethod, + debugTime: UInt64 ) -> URLRequest where TypeLock: GraphQLOperation & Decodable { // Construct a request. var request = URLRequest(url: url) @@ -198,7 +235,19 @@ private func createGraphQLRequest( // Construct HTTP body. let encoder = JSONEncoder() let payload = selection.buildPayload(operationName: operationName) - request.httpBody = try! encoder.encode(payload) + + #if DEBUG + #if targetEnvironment(simulator) + // Write the query + try? payload.query.write(toFile: "/tmp/query_\(debugTime).graphql", atomically: true, encoding: .utf8) + // Write the variables + if let variables = try? encoder.encode(payload.variables) { + try? variables.write(to: URL(fileURLWithPath: "/tmp/query_variables_\(debugTime).json")) + } + #endif + #endif + let encoded = try! encoder.encode(payload) + request.httpBody = encoded return request } diff --git a/Sources/SwiftGraphQL/Result.swift b/Sources/SwiftGraphQL/Result.swift index 83e77399..3e27b5b3 100644 --- a/Sources/SwiftGraphQL/Result.swift +++ b/Sources/SwiftGraphQL/Result.swift @@ -5,6 +5,7 @@ import Foundation public struct GraphQLResult { public let data: Type public let errors: [GraphQLError]? + public let extensions: [String: AnyCodable]? } extension GraphQLResult: Equatable where Type: Equatable, TypeLock: Decodable {} @@ -12,14 +13,23 @@ extension GraphQLResult: Equatable where Type: Equatable, TypeLock: Decodable {} extension GraphQLResult where TypeLock: Decodable { init(_ response: Data, with selection: Selection) throws { // Decodes the data using provided selection. + var errors: [GraphQLError]? = nil + var extensions: [String: AnyCodable]? = nil do { let decoder = JSONDecoder() let response = try decoder.decode(GraphQLResponse.self, from: response) + errors = response.errors + extensions = response.extensions self.data = try selection.decode(data: response.data) - self.errors = response.errors - } catch { - // Catches all errors and turns them into a bad payload SwiftGraphQL error. - throw HttpError.badpayload + self.errors = errors + self.extensions = extensions + } catch let error { + // If we have specific errors, use them + if let errors = errors, !errors.isEmpty { + throw HttpError.graphQLErrors(errors, extensions: extensions) + } else { + throw HttpError.decodingError(error, extensions: extensions) + } } } @@ -29,6 +39,7 @@ extension GraphQLResult where TypeLock: Decodable { let response: GraphQLResponse = try webSocketMessage.decodePayload() self.data = try selection.decode(data: response.data) self.errors = response.errors + self.extensions = response.extensions } catch { // Catches all errors and turns them into a bad payload SwiftGraphQL error. throw HttpError.badpayload @@ -39,6 +50,7 @@ extension GraphQLResult where TypeLock: Decodable { struct GraphQLResponse: Decodable { let data: TypeLock? + let extensions: [String: AnyCodable]? let errors: [GraphQLError]? } } @@ -46,9 +58,9 @@ extension GraphQLResult where TypeLock: Decodable { // MARK: - GraphQL Error public struct GraphQLError: Codable, Equatable { - let message: String + public let message: String public let locations: [Location]? -// public let path: [String]? + public let extensions: [String: AnyCodable]? public struct Location: Codable, Equatable { public let line: Int diff --git a/Sources/SwiftGraphQLCLI/main.swift b/Sources/SwiftGraphQLCLI/main.swift index a959713d..13e9e3a1 100755 --- a/Sources/SwiftGraphQLCLI/main.swift +++ b/Sources/SwiftGraphQLCLI/main.swift @@ -12,7 +12,7 @@ struct SwiftGraphQLCLI: ParsableCommand { // MARK: - Parameters @Argument(help: "GraphQL server endpoint.") - var endpoint: String + var endpoint: String? @Option(help: "Relative path from CWD to your YML config file.") var config: String? @@ -32,11 +32,6 @@ struct SwiftGraphQLCLI: ParsableCommand { // MARK: - Main func run() throws { - // Make sure we get a valid endpoint to fetch. - guard let url = URL(string: endpoint) else { - SwiftGraphQLCLI.exit(withError: SwiftGraphQLGeneratorError.endpoint) - } - // Load configuration if config path is present, otherwise use default. let config: Config @@ -47,24 +42,29 @@ struct SwiftGraphQLCLI: ParsableCommand { config = Config() } - var headers: [String: String] = [:] - - if let authorization = authorization { - headers["Authorization"] = authorization - } - - // Generate the code. let generator = GraphQLCodegen(scalars: config.scalars) - let code = try generator.generate(from: url, withHeaders: headers) - // Write to target file or stdout. + let code: String + if let url = endpoint { + guard let url = URL(string: url) else { + SwiftGraphQLCLI.exit(withError: SwiftGraphQLGeneratorError.endpoint) + } + var headers: [String: String] = [:] + + if let authorization = authorization { + headers["Authorization"] = authorization + } + + code = try generator.generate(from: url, withHeaders: headers) + } else { + code = try generator.generateFromStdin() + } + if let outputPath = output { try Folder.current.createFile(at: outputPath).write(code) } else { FileHandle.standardOutput.write(code.data(using: .utf8)!) } - - // The end } } diff --git a/Sources/SwiftGraphQLCodegen/Generator.swift b/Sources/SwiftGraphQLCodegen/Generator.swift index fe4645e5..a5176f07 100644 --- a/Sources/SwiftGraphQLCodegen/Generator.swift +++ b/Sources/SwiftGraphQLCodegen/Generator.swift @@ -28,6 +28,13 @@ public struct GraphQLCodegen { let code = try generate(schema: schema) return code } + + public func generateFromStdin() throws -> String { + let introspection: Data = try FileHandle.standardInput.readToEnd()! + let schema = try Schema(introspection: introspection) + let code = try generate(schema: schema) + return code + } /// Generates the code that can be used to define selections. func generate(schema: Schema) throws -> String { diff --git a/Tests/SwiftGraphQLTests/ResultTests.swift b/Tests/SwiftGraphQLTests/ResultTests.swift index 4cdfd857..5346d9de 100644 --- a/Tests/SwiftGraphQLTests/ResultTests.swift +++ b/Tests/SwiftGraphQLTests/ResultTests.swift @@ -57,7 +57,7 @@ final class ParserTests: XCTestCase { XCTAssertEqual(result.errors, [ GraphQLError( message: "Message.", - locations: [GraphQLError.Location(line: 6, column: 7)] + locations: [GraphQLError.Location(line: 6, column: 7)], extensions: [:] ), ]) }