Skip to content

Commit 8693767

Browse files
author
Clément Le Provost
authored
Add support for Algolia Places (#153)
Fixes #152. Quite a bit of refactoring was involved, as Places requires different hosts, a specific endpoints, and different query parameters. - Create abstract base classes `AbstractClient` and `AbstractQuery` (although there is no such thing as an abstract class in Swift…) - Move custom behavior out of `Client` and `Query` into those classes - Create `PlacesClient` and `PlacesQuery` - Implement the `/1/places/query` endpoint (`search()` method) - Add test cases - Adapt test script to handle the Places-specific credentials * Hide the `PlacesClient` initializer with optionals … and add a new convenience initializer with non-optionals. There is now a clean choice between no credentials at all or two non-optional credentials. * [doc] Update documentation * [doc] Fix documentation * Fix infinite recursion when calling from Objective-C Although a specific selector name has been assigned to the overloaded constructor, it does not seem to work. * [test] Test that results are not empty
1 parent 4706b28 commit 8693767

15 files changed

+1432
-568
lines changed

.jazzy.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ custom_categories:
1515
- Index
1616
- Query
1717
- BrowseIterator
18+
- name: Places
19+
children:
20+
- PlacesClient
21+
- PlacesQuery
1822
- name: Auxiliary types
1923
children:
2024
- GeoRect

AlgoliaSearch.xcodeproj/project.pbxproj

Lines changed: 76 additions & 0 deletions
Large diffs are not rendered by default.

Source/AbstractClient.swift

Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
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

Comments
 (0)