|
| 1 | +// |
| 2 | +// Copyright (c) 2016 Algolia |
| 3 | +// http://www.algolia.com/ |
| 4 | +// |
| 5 | +// Permission is hereby granted, free of charge, to any person obtaining a copy |
| 6 | +// of this software and associated documentation files (the "Software"), to deal |
| 7 | +// in the Software without restriction, including without limitation the rights |
| 8 | +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
| 9 | +// copies of the Software, and to permit persons to whom the Software is |
| 10 | +// furnished to do so, subject to the following conditions: |
| 11 | +// |
| 12 | +// The above copyright notice and this permission notice shall be included in |
| 13 | +// all copies or substantial portions of the Software. |
| 14 | +// |
| 15 | +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
| 16 | +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
| 17 | +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
| 18 | +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
| 19 | +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
| 20 | +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
| 21 | +// THE SOFTWARE. |
| 22 | +// |
| 23 | + |
| 24 | +import Foundation |
| 25 | + |
| 26 | + |
| 27 | +/// A version of a software library. |
| 28 | +/// Used to construct the `User-Agent` header. |
| 29 | +/// |
| 30 | +@objc public class LibraryVersion: NSObject { |
| 31 | + /// Library name. |
| 32 | + @objc public let name: String |
| 33 | + |
| 34 | + /// Version string. |
| 35 | + @objc public let version: String |
| 36 | + |
| 37 | + @objc public init(name: String, version: String) { |
| 38 | + self.name = name |
| 39 | + self.version = version |
| 40 | + } |
| 41 | + |
| 42 | + // MARK: Equatable |
| 43 | + |
| 44 | + override public func isEqual(_ object: Any?) -> Bool { |
| 45 | + if let rhs = object as? LibraryVersion { |
| 46 | + return self.name == rhs.name && self.version == rhs.version |
| 47 | + } else { |
| 48 | + return false |
| 49 | + } |
| 50 | + } |
| 51 | +} |
| 52 | + |
| 53 | + |
| 54 | +/// An abstract API client. |
| 55 | +/// |
| 56 | +/// + Warning: Not meant to be used directly. See `Client` or `PlacesClient` instead. |
| 57 | +/// |
| 58 | +@objc public class AbstractClient : NSObject { |
| 59 | + // MARK: Properties |
| 60 | + |
| 61 | + /// HTTP headers that will be sent with every request. |
| 62 | + @objc public var headers = [String:String]() |
| 63 | + |
| 64 | + /// Algolia API key. |
| 65 | + /// |
| 66 | + /// + Note: Optional version, for internal use only. |
| 67 | + /// |
| 68 | + @objc internal var _apiKey: String? { |
| 69 | + didSet { |
| 70 | + updateHeadersFromAPIKey() |
| 71 | + } |
| 72 | + } |
| 73 | + private func updateHeadersFromAPIKey() { |
| 74 | + headers["X-Algolia-API-Key"] = _apiKey |
| 75 | + } |
| 76 | + |
| 77 | + /// The list of libraries used by this client, passed in the `User-Agent` HTTP header of every request. |
| 78 | + /// It is initially set to contain only this API Client, but may be overridden to include other libraries. |
| 79 | + /// |
| 80 | + /// + WARNING: The user agent is crucial to proper statistics in your Algolia dashboard. Please leave it as is. |
| 81 | + /// This field is publicly exposed only for the sake of other Algolia libraries. |
| 82 | + /// |
| 83 | + @objc public var userAgents: [LibraryVersion] = [] { |
| 84 | + didSet { |
| 85 | + updateHeadersFromUserAgents() |
| 86 | + } |
| 87 | + } |
| 88 | + private func updateHeadersFromUserAgents() { |
| 89 | + headers["User-Agent"] = userAgents.map({ return "\($0.name) (\($0.version))"}).joined(separator: "; ") |
| 90 | + } |
| 91 | + |
| 92 | + /// Default timeout for network requests. Default: 30 seconds. |
| 93 | + @objc public var timeout: TimeInterval = 30 |
| 94 | + |
| 95 | + /// Specific timeout for search requests. Default: 5 seconds. |
| 96 | + @objc public var searchTimeout: TimeInterval = 5 |
| 97 | + |
| 98 | + /// Algolia application ID. |
| 99 | + /// |
| 100 | + /// + Note: Optional version, for internal use only. |
| 101 | + /// |
| 102 | + @objc internal let _appID: String? |
| 103 | + |
| 104 | + /// Hosts for read queries, in priority order. |
| 105 | + /// The first host will always be used, then subsequent hosts in case of retry. |
| 106 | + /// |
| 107 | + /// + Warning: The default values should be appropriate for most use cases. |
| 108 | + /// Change them only if you know what you are doing. |
| 109 | + /// |
| 110 | + @objc public var readHosts: [String] { |
| 111 | + willSet { |
| 112 | + assert(!newValue.isEmpty) |
| 113 | + } |
| 114 | + } |
| 115 | + |
| 116 | + /// Hosts for write queries, in priority order. |
| 117 | + /// The first host will always be used, then subsequent hosts in case of retry. |
| 118 | + /// |
| 119 | + /// + Warning: The default values should be appropriate for most use cases. |
| 120 | + /// Change them only if you know what you are doing. |
| 121 | + /// |
| 122 | + @objc public var writeHosts: [String] { |
| 123 | + willSet { |
| 124 | + assert(!newValue.isEmpty) |
| 125 | + } |
| 126 | + } |
| 127 | + |
| 128 | + // NOTE: Not constant only for the sake of mocking during unit tests. |
| 129 | + var session: URLSession |
| 130 | + |
| 131 | + /// Operation queue used to keep track of requests. |
| 132 | + /// `Request` instances are inherently asynchronous, since they are merely wrappers around `NSURLSessionTask`. |
| 133 | + /// The sole purpose of the queue is to retain them for the duration of their execution! |
| 134 | + /// |
| 135 | + let requestQueue: OperationQueue |
| 136 | + |
| 137 | + /// Dispatch queue used to run completion handlers. |
| 138 | + internal var completionQueue = DispatchQueue.main |
| 139 | + |
| 140 | + // MARK: Initialization |
| 141 | + |
| 142 | + internal init(appID: String?, apiKey: String?, readHosts: [String], writeHosts: [String]) { |
| 143 | + self._appID = appID |
| 144 | + self._apiKey = apiKey |
| 145 | + self.readHosts = readHosts |
| 146 | + self.writeHosts = writeHosts |
| 147 | + |
| 148 | + // WARNING: Those headers cannot be changed for the lifetime of the session. |
| 149 | + // Other headers are likely to change during the lifetime of the session: they will be passed at every request. |
| 150 | + var fixedHTTPHeaders: [String: String] = [:] |
| 151 | + fixedHTTPHeaders["X-Algolia-Application-Id"] = self._appID |
| 152 | + let configuration = URLSessionConfiguration.default |
| 153 | + configuration.httpAdditionalHeaders = fixedHTTPHeaders |
| 154 | + session = Foundation.URLSession(configuration: configuration) |
| 155 | + |
| 156 | + requestQueue = OperationQueue() |
| 157 | + requestQueue.maxConcurrentOperationCount = 8 |
| 158 | + |
| 159 | + super.init() |
| 160 | + |
| 161 | + // Add this library's version to the user agents. |
| 162 | + let version = Bundle(for: type(of: self)).infoDictionary!["CFBundleShortVersionString"] as! String |
| 163 | + self.userAgents = [ LibraryVersion(name: "Algolia for Swift", version: version) ] |
| 164 | + |
| 165 | + // Add the operating system's version to the user agents. |
| 166 | + if #available(iOS 8.0, OSX 10.0, tvOS 9.0, *) { |
| 167 | + let osVersion = ProcessInfo.processInfo.operatingSystemVersion |
| 168 | + var osVersionString = "\(osVersion.majorVersion).\(osVersion.minorVersion)" |
| 169 | + if osVersion.patchVersion != 0 { |
| 170 | + osVersionString += ".\(osVersion.patchVersion)" |
| 171 | + } |
| 172 | + if let osName = osName { |
| 173 | + self.userAgents.append(LibraryVersion(name: osName, version: osVersionString)) |
| 174 | + } |
| 175 | + } |
| 176 | + |
| 177 | + // WARNING: `didSet` not called during initialization => we need to update the headers manually here. |
| 178 | + updateHeadersFromAPIKey() |
| 179 | + updateHeadersFromUserAgents() |
| 180 | + } |
| 181 | + |
| 182 | + /// Set read and write hosts to the same value (convenience method). |
| 183 | + /// |
| 184 | + /// + Warning: The default values should be appropriate for most use cases. |
| 185 | + /// Change them only if you know what you are doing. |
| 186 | + /// |
| 187 | + @objc(setHosts:) |
| 188 | + public func setHosts(_ hosts: [String]) { |
| 189 | + readHosts = hosts |
| 190 | + writeHosts = hosts |
| 191 | + } |
| 192 | + |
| 193 | + /// Set an HTTP header that will be sent with every request. |
| 194 | + /// |
| 195 | + /// + Note: You may also use the `headers` property directly. |
| 196 | + /// |
| 197 | + /// - parameter name: Header name. |
| 198 | + /// - parameter value: Value for the header. If `nil`, the header will be removed. |
| 199 | + /// |
| 200 | + @objc(setHeaderWithName:to:) |
| 201 | + public func setHeader(withName name: String, to value: String?) { |
| 202 | + headers[name] = value |
| 203 | + } |
| 204 | + |
| 205 | + /// Get an HTTP header. |
| 206 | + /// |
| 207 | + /// + Note: You may also use the `headers` property directly. |
| 208 | + /// |
| 209 | + /// - parameter name: Header name. |
| 210 | + /// - returns: The header's value, or `nil` if the header does not exist. |
| 211 | + /// |
| 212 | + @objc(headerWithName:) |
| 213 | + public func header(withName name: String) -> String? { |
| 214 | + return headers[name] |
| 215 | + } |
| 216 | + |
| 217 | + // MARK: - Operations |
| 218 | + |
| 219 | + /// Ping the server. |
| 220 | + /// This method returns nothing except a message indicating that the server is alive. |
| 221 | + /// |
| 222 | + /// - parameter completionHandler: Completion handler to be notified of the request's outcome. |
| 223 | + /// - returns: A cancellable operation. |
| 224 | + /// |
| 225 | + @objc(isAlive:) |
| 226 | + @discardableResult public func isAlive(completionHandler: @escaping CompletionHandler) -> Operation { |
| 227 | + let path = "1/isalive" |
| 228 | + return performHTTPQuery(path: path, method: .GET, body: nil, hostnames: readHosts, completionHandler: completionHandler) |
| 229 | + } |
| 230 | + |
| 231 | + // MARK: - Network |
| 232 | + |
| 233 | + /// Perform an HTTP Query. |
| 234 | + func performHTTPQuery(path: String, method: HTTPMethod, body: JSONObject?, hostnames: [String], isSearchQuery: Bool = false, completionHandler: CompletionHandler? = nil) -> Operation { |
| 235 | + let request = newRequest(method: method, path: path, body: body, hostnames: hostnames, isSearchQuery: isSearchQuery, completion: completionHandler) |
| 236 | + request.completionQueue = self.completionQueue |
| 237 | + requestQueue.addOperation(request) |
| 238 | + return request |
| 239 | + } |
| 240 | + |
| 241 | + /// Create a request with this client's settings. |
| 242 | + func newRequest(method: HTTPMethod, path: String, body: JSONObject?, hostnames: [String], isSearchQuery: Bool = false, completion: CompletionHandler? = nil) -> Request { |
| 243 | + let currentTimeout = isSearchQuery ? searchTimeout : timeout |
| 244 | + let request = Request(session: session, method: method, hosts: hostnames, firstHostIndex: 0, path: path, headers: headers, jsonBody: body, timeout: currentTimeout, completion: completion) |
| 245 | + return request |
| 246 | + } |
| 247 | +} |
0 commit comments