aboutsummaryrefslogtreecommitdiff
path: root/Sources/swiftGopherClient/gopherClient.swift
blob: 3fd61ec808dfc0c2933297be5906f62660bee523 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
//
//  gopherClient.swift
//
//
//  Created by Navan Chauhan on 12/12/23.
//

import Foundation
import GopherHelpers
import NIO
import NIOTransportServices

/// `GopherClient` is a class for handling network connections and requests to Gopher servers.
///
/// This client utilizes Swift NIO for efficient, non-blocking network operations. It automatically
/// chooses the appropriate `EventLoopGroup` based on the running platform:
/// - On iOS/macOS 10.14+, it uses `NIOTSEventLoopGroup` for optimal performance.
/// - On Linux or older Apple platforms, it falls back to `MultiThreadedEventLoopGroup`.
///
/// The client supports both synchronous (completion handler-based) and asynchronous (Swift concurrency) APIs
/// for sending requests to Gopher servers.
public class GopherClient {
    /// The event loop group used for managing network operations.
    private let group: EventLoopGroup

    /// Initializes a new instance of `GopherClient`.
    ///
    /// This initializer automatically selects the appropriate `EventLoopGroup` based on the running platform.
    public init() {
        #if os(Linux)
            self.group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount)
        #else
            if #available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, visionOS 1.0, *) {
                self.group = NIOTSEventLoopGroup()
            } else {
                self.group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount)
            }
        #endif
    }

    /// Cleans up resources when the instance is deinitialized.
    deinit {
        self.shutdownEventLoopGroup()
    }

    /// Sends a request to a Gopher server using a completion handler.
    ///
    /// This method asynchronously establishes a connection, sends the request, and calls the completion
    /// handler with the result.
    ///
    /// - 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. It takes a `Result` type
    ///     which either contains an array of `gopherItem` on success or an `Error` on failure.
    public func sendRequest(
        to host: String,
        port: Int = 70,
        message: String,
        completion: @escaping (Result<[gopherItem], Error>) -> Void
    ) {
        let bootstrap = self.createBootstrap(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 using Swift concurrency.
    ///
    /// This method asynchronously establishes a connection and sends the request,
    /// returning the result as an array of `gopherItem`.
    ///
    /// - 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.
    ///
    /// - Returns: An array of `gopherItem` representing the server's response.
    ///
    /// - Throws: An error if the connection fails or the server returns an invalid response.
    @available(iOS 13.0, *)
    @available(macOS 10.15, *)
    public func sendRequest(to host: String, port: Int = 70, message: String) async throws
        -> [gopherItem]
    {
        return try await withCheckedThrowingContinuation { continuation in
            let bootstrap = self.createBootstrap(message: message) { result in
                continuation.resume(with: result)
            }

            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):
                    continuation.resume(throwing: error)
                }
            }
        }
    }

    /// Creates a bootstrap for connecting to a Gopher server.
    ///
    /// This method sets up the appropriate bootstrap based on the platform and configures
    /// the channel with a `GopherRequestResponseHandler`.
    ///
    /// - Parameters:
    ///   - message: The message to be sent to the server.
    ///   - completion: A closure that handles the result of the request.
    ///
    /// - Returns: A `NIOClientTCPBootstrapProtocol` configured for Gopher communication.
    private func createBootstrap(
        message: String,
        completion: @escaping (Result<[gopherItem], Error>) -> Void
    ) -> NIOClientTCPBootstrapProtocol {
        let handler = GopherRequestResponseHandler(message: message, completion: completion)

        #if os(Linux)
            return ClientBootstrap(group: group)
                .channelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1)
                .channelInitializer { channel in
                    channel.pipeline.addHandler(handler)
                }
        #else
            if #available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, visionOS 1.0, *) {
                return NIOTSConnectionBootstrap(group: group)
                    .channelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1)
                    .channelInitializer { channel in
                        channel.pipeline.addHandler(handler)
                    }
            } else {
                return ClientBootstrap(group: group)
                    .channelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1)
                    .channelInitializer { channel in
                        channel.pipeline.addHandler(handler)
                    }
            }
        #endif
    }

    /// 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)")
        }
    }
}