aboutsummaryrefslogtreecommitdiff
path: root/Sources/swiftGopherClient
diff options
context:
space:
mode:
Diffstat (limited to 'Sources/swiftGopherClient')
-rw-r--r--Sources/swiftGopherClient/gopherClient.swift129
-rw-r--r--Sources/swiftGopherClient/gopherRequestResponseHandler.swift162
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))
+ }
+}