diff options
Diffstat (limited to 'Sources/swiftGopherClient')
-rw-r--r-- | Sources/swiftGopherClient/gopherClient.swift | 129 | ||||
-rw-r--r-- | Sources/swiftGopherClient/gopherRequestResponseHandler.swift | 162 |
2 files changed, 159 insertions, 132 deletions
diff --git a/Sources/swiftGopherClient/gopherClient.swift b/Sources/swiftGopherClient/gopherClient.swift index eabbbc4..c7542ee 100644 --- a/Sources/swiftGopherClient/gopherClient.swift +++ b/Sources/swiftGopherClient/gopherClient.swift @@ -6,67 +6,90 @@ // import Foundation +import GopherHelpers import NIO import NIOTransportServices -import GopherHelpers +/// `GopherClient` is a class for handling network connections and requests to Gopher servers. +/// +/// It utilizes `NIOTSEventLoopGroup` on iOS/macOS (Not sure why you would run this on watchOS/tvOS but it supports that as well) for network operations, falling back to `MultiThreadedEventLoopGroup` otherwise. public class GopherClient { - private let group: EventLoopGroup + private let group: EventLoopGroup - public init() { - if #available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) { - self.group = NIOTSEventLoopGroup() - } else { - self.group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - } + /// Initializes a new instance of `GopherClient`. + /// + /// It automatically chooses the appropriate `EventLoopGroup` based on the running platform. + public init() { + if #available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) { + self.group = NIOTSEventLoopGroup() + } else { + self.group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) } + } - deinit { - try? group.syncShutdownGracefully() - } + deinit { + try? group.syncShutdownGracefully() + } - public func sendRequest(to host: String, port: Int = 70, message: String, completion: @escaping (Result<[gopherItem], Error>) -> Void) { - if #available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) { - let bootstrap = NIOTSConnectionBootstrap(group: group) - .channelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1) - .channelInitializer { channel in - channel.pipeline.addHandler(GopherRequestResponseHandler(message: message, completion: completion)) - } - bootstrap.connect(host: host, port: port).whenComplete { result in - switch result { - case .success(let channel): - channel.closeFuture.whenComplete { _ in - print("Connection closed") - } - case .failure(let error): - completion(.failure(error)) - } - } - } else { - let bootstrap = ClientBootstrap(group: group) - .channelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1) - .channelInitializer { channel in - channel.pipeline.addHandler(GopherRequestResponseHandler(message: message, completion: completion)) - } - bootstrap.connect(host: host, port: port).whenComplete { result in - switch result { - case .success(let channel): - channel.closeFuture.whenComplete { _ in - print("Connection closed") - } - case .failure(let error): - completion(.failure(error)) - } - } + /// Sends a request to a Gopher server. + /// + /// - Parameters: + /// - host: The host address of the Gopher server. + /// - port: The port of the Gopher server. Defaults to 70. + /// - message: The message to be sent to the server. + /// - completion: A closure that handles the result of the request. + /// + /// The method asynchronously establishes a connection, sends the request, and calls the completion handler with the result. + public func sendRequest( + to host: String, port: Int = 70, message: String, + completion: @escaping (Result<[gopherItem], Error>) -> Void + ) { + if #available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) { + let bootstrap = NIOTSConnectionBootstrap(group: group) + .channelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1) + .channelInitializer { channel in + channel.pipeline.addHandler( + GopherRequestResponseHandler(message: message, completion: completion)) } - - } - - private func shutdownEventLoopGroup() { - do { - try group.syncShutdownGracefully() - } catch { - print("Error shutting down event loop group: \(error)") - } + bootstrap.connect(host: host, port: port).whenComplete { result in + switch result { + case .success(let channel): + channel.closeFuture.whenComplete { _ in + print("Connection closed") + } + case .failure(let error): + completion(.failure(error)) + } + } + } else { + let bootstrap = ClientBootstrap(group: group) + .channelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1) + .channelInitializer { channel in + channel.pipeline.addHandler( + GopherRequestResponseHandler(message: message, completion: completion)) } + bootstrap.connect(host: host, port: port).whenComplete { result in + switch result { + case .success(let channel): + channel.closeFuture.whenComplete { _ in + print("Connection closed") + } + case .failure(let error): + completion(.failure(error)) + } + } + } + + } + + /// Shuts down the event loop group, releasing any resources. + /// + /// This method is called during deinitialization to ensure clean shutdown of network resources. + private func shutdownEventLoopGroup() { + do { + try group.syncShutdownGracefully() + } catch { + print("Error shutting down event loop group: \(error)") + } + } } diff --git a/Sources/swiftGopherClient/gopherRequestResponseHandler.swift b/Sources/swiftGopherClient/gopherRequestResponseHandler.swift index d69c8c8..6cede51 100644 --- a/Sources/swiftGopherClient/gopherRequestResponseHandler.swift +++ b/Sources/swiftGopherClient/gopherRequestResponseHandler.swift @@ -6,94 +6,98 @@ // import Foundation -import NIO import GopherHelpers +import NIO final class GopherRequestResponseHandler: ChannelInboundHandler { - typealias InboundIn = ByteBuffer - typealias OutboundOut = ByteBuffer + typealias InboundIn = ByteBuffer + typealias OutboundOut = ByteBuffer - private var accumulatedData: ByteBuffer - private let message: String - private let completion: (Result<[gopherItem], Error>) -> Void + private var accumulatedData: ByteBuffer + private let message: String + private let completion: (Result<[gopherItem], Error>) -> Void - init(message: String, completion: @escaping (Result<[gopherItem], Error>) -> Void) { - self.message = message - self.completion = completion - self.accumulatedData = ByteBuffer() - } + init(message: String, completion: @escaping (Result<[gopherItem], Error>) -> Void) { + self.message = message + self.completion = completion + self.accumulatedData = ByteBuffer() + } - func channelActive(context: ChannelHandlerContext) { - var buffer = context.channel.allocator.buffer(capacity: message.utf8.count) - buffer.writeString(message) - context.writeAndFlush(self.wrapOutboundOut(buffer), promise: nil) - } + func channelActive(context: ChannelHandlerContext) { + var buffer = context.channel.allocator.buffer(capacity: message.utf8.count) + buffer.writeString(message) + context.writeAndFlush(self.wrapOutboundOut(buffer), promise: nil) + } - func channelRead(context: ChannelHandlerContext, data: NIOAny) { - var buffer = unwrapInboundIn(data) - accumulatedData.writeBuffer(&buffer) - } - - func channelInactive(context: ChannelHandlerContext) { - if let dataCopy = accumulatedData.getSlice(at: 0, length: accumulatedData.readableBytes) { - parseGopherServerResponse(response: accumulatedData.readString(length: accumulatedData.readableBytes) ?? "", originalBytes: dataCopy) - } - } - - func errorCaught(context: ChannelHandlerContext, error: Error) { - print("Error: ", error) - context.close(promise: nil) - } - - func createGopherItem(rawLine: String, itemType: gopherItemType = .info, rawData: ByteBuffer) -> gopherItem { - var item = gopherItem(rawLine: rawLine) - item.parsedItemType = itemType - item.rawData = rawData - - if rawLine.isEmpty { - item.valid = false - } else { - let components = rawLine.components(separatedBy: "\t") - - // Handle cases where rawLine does not have any itemType in the first character - item.message = String(components[0].dropFirst()) - - if components.indices.contains(1) { - item.selector = String(components[1]) - } - - if components.indices.contains(2) { - item.host = String(components[2]) - } - - if components.indices.contains(3) { - item.port = Int(String(components[3])) ?? 70 - } - } - - return item + func channelRead(context: ChannelHandlerContext, data: NIOAny) { + var buffer = unwrapInboundIn(data) + accumulatedData.writeBuffer(&buffer) + } + + func channelInactive(context: ChannelHandlerContext) { + if let dataCopy = accumulatedData.getSlice(at: 0, length: accumulatedData.readableBytes) { + parseGopherServerResponse( + response: accumulatedData.readString(length: accumulatedData.readableBytes) ?? "", + originalBytes: dataCopy) } - - func parseGopherServerResponse(response: String, originalBytes: ByteBuffer) { - var gopherServerResponse: [gopherItem] = [] - - print("parsing") - let carriageReturnCount = response.filter({ $0 == "\r" }).count - let newlineCarriageReturnCount = response.filter({ $0 == "\r\n" }).count - print("Carriage Returns: \(carriageReturnCount), Newline + Carriage Returns: \(newlineCarriageReturnCount)") - - for line in response.split(separator: "\r\n") { - let lineItemType = getGopherFileType(item: "\(line.first ?? " ")") - let item = createGopherItem(rawLine: String(line), itemType: lineItemType, rawData: originalBytes) - gopherServerResponse.append(item) - - } - - print("done parsing") - - completion(.success(gopherServerResponse)) + } + + func errorCaught(context: ChannelHandlerContext, error: Error) { + print("Error: ", error) + context.close(promise: nil) + } + + func createGopherItem(rawLine: String, itemType: gopherItemType = .info, rawData: ByteBuffer) + -> gopherItem + { + var item = gopherItem(rawLine: rawLine) + item.parsedItemType = itemType + item.rawData = rawData + + if rawLine.isEmpty { + item.valid = false + } else { + let components = rawLine.components(separatedBy: "\t") + + // Handle cases where rawLine does not have any itemType in the first character + item.message = String(components[0].dropFirst()) + + if components.indices.contains(1) { + item.selector = String(components[1]) + } + + if components.indices.contains(2) { + item.host = String(components[2]) + } + + if components.indices.contains(3) { + item.port = Int(String(components[3])) ?? 70 + } } -} + return item + } + + func parseGopherServerResponse(response: String, originalBytes: ByteBuffer) { + var gopherServerResponse: [gopherItem] = [] + print("parsing") + let carriageReturnCount = response.filter({ $0 == "\r" }).count + let newlineCarriageReturnCount = response.filter({ $0 == "\r\n" }).count + print( + "Carriage Returns: \(carriageReturnCount), Newline + Carriage Returns: \(newlineCarriageReturnCount)" + ) + for line in response.split(separator: "\r\n") { + let lineItemType = getGopherFileType(item: "\(line.first ?? " ")") + let item = createGopherItem( + rawLine: String(line), itemType: lineItemType, rawData: originalBytes) + gopherServerResponse.append(item) + + } + + print("done parsing") + + completion(.success(gopherServerResponse)) + } +} |