Commit 2665b8a0 by Dmitriy Stepanets

- Added coordinators

- Started implementing new Neumorphic UI
parent 637f28d0
......@@ -7,7 +7,7 @@
<key>1Weather.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>4</integer>
<integer>5</integer>
</dict>
<key>PG (Playground) 1.xcscheme</key>
<dict>
......
......@@ -4,12 +4,54 @@
<dict>
<key>SchemeUserState</key>
<dict>
<key>PG (Playground) 1.xcscheme</key>
<dict>
<key>isShown</key>
<false/>
<key>orderHint</key>
<integer>1</integer>
</dict>
<key>PG (Playground) 2.xcscheme</key>
<dict>
<key>isShown</key>
<false/>
<key>orderHint</key>
<integer>2</integer>
</dict>
<key>PG (Playground) 3.xcscheme</key>
<dict>
<key>isShown</key>
<false/>
<key>orderHint</key>
<integer>3</integer>
</dict>
<key>PG (Playground) 4.xcscheme</key>
<dict>
<key>isShown</key>
<false/>
<key>orderHint</key>
<integer>10</integer>
</dict>
<key>PG (Playground) 5.xcscheme</key>
<dict>
<key>isShown</key>
<false/>
<key>orderHint</key>
<integer>11</integer>
</dict>
<key>PG (Playground) 6.xcscheme</key>
<dict>
<key>isShown</key>
<false/>
<key>orderHint</key>
<integer>12</integer>
</dict>
<key>PG (Playground).xcscheme</key>
<dict>
<key>isShown</key>
<false/>
<key>orderHint</key>
<integer>5</integer>
<integer>0</integer>
</dict>
</dict>
</dict>
......
......@@ -16,8 +16,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
ThemeManager.refreshAppearance()
self.window = UIWindow(frame: UIScreen.main.bounds)
self.window?.rootViewController = InitialViewController()
self.window?.makeKeyAndVisible()
let appCoordinator = AppCoordinator(window: self.window!)
appCoordinator.start()
return true
}
......
//
// AppCoordinator.swift
// 1Weather
//
// Created by Dmitry Stepanets on 08.02.2021.
//
import UIKit
class AppCoordinator: Coordinator {
//Private
private let tabBarController = UITabBarController()
//Public
var parentCoordinator: Coordinator?
var childCoordinators = [Coordinator]()
init(window:UIWindow) {
window.rootViewController = tabBarController
window.makeKeyAndVisible()
}
func start() {
let todayCoordinator = TodayCoordinator(tabBarController: tabBarController)
todayCoordinator.start()
childCoordinators.append(todayCoordinator)
let forecastCoordinator = ForecastCoordinator(tabBarController: tabBarController)
forecastCoordinator.start()
childCoordinators.append(forecastCoordinator)
}
}
//
// CoordinatorProtocol.swift
// 1Weather
//
// Created by Dmitry Stepanets on 30.06.2020.
// Copyright © 2020 Dynamix Software. All rights reserved.
//
import UIKit
protocol Coordinator: AnyObject {
var childCoordinators: [Coordinator] { get set }
var parentCoordinator:Coordinator? { get set }
func start()
}
extension Coordinator {
func childDidFinish(child:Coordinator) {
for (index, coordinator) in childCoordinators.enumerated() {
if coordinator === child {
childCoordinators.remove(at: index)
break
}
}
}
}
//
// ForecastCoordinator.swift
// 1Weather
//
// Created by Dmitry Stepanets on 08.02.2021.
//
import UIKit
class ForecastCoordinator: Coordinator {
//Private
private var tabBarController:UITabBarController?
//Public
var childCoordinators = [Coordinator]()
var parentCoordinator: Coordinator?
init(tabBarController:UITabBarController) {
self.tabBarController = tabBarController
}
func start() {
let forecastViewController = ForecastViewController()
self.tabBarController?.add(viewController: forecastViewController)
}
}
//
// TodayCoordinator.swift
// 1Weather
//
// Created by Dmitry Stepanets on 08.02.2021.
//
import UIKit
class TodayCoordinator:Coordinator {
//Private
private let navigationController = UINavigationController(nibName: nil, bundle: nil)
private var tabBarController:UITabBarController?
//Public
var childCoordinators = [Coordinator]()
var parentCoordinator: Coordinator?
init(tabBarController:UITabBarController) {
self.tabBarController = tabBarController
}
func start() {
let todayViewController = TodayViewController()
navigationController.viewControllers = [todayViewController]
tabBarController?.add(viewController: navigationController)
}
}
//
// UITabBarController+Append.swift
// 1Weather
//
// Created by Dmitry Stepanets on 08.02.2021.
//
import UIKit
extension UITabBarController {
func add(viewController:UIViewController) {
var existingControllers = [UIViewController]()
existingControllers.append(contentsOf: self.viewControllers ?? [UIViewController]())
existingControllers.append(viewController)
self.viewControllers = existingControllers
}
}
//
// InitialViewController.swift
// 1Weather
//
// Created by Dmitry Stepanets on 04.12.2020.
//
import UIKit
class InitialViewController: UIViewController {
let searchButton = SelfSizingButton()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
prepareSearchButton()
}
@objc private func handleSearchButton() {
let locationVC = LocationViewController(closeButtonIsHidden: false)
self.present(locationVC, animated: true)
}
}
//MARK:- Prepare
private extension InitialViewController {
func prepareSearchButton() {
searchButton.setTitle("Open Search controller", for: .normal)
searchButton.setTitleColor(.black, for: .normal)
searchButton.backgroundColor = .lightGray
searchButton.addTarget(self, action: #selector(handleSearchButton), for: .touchUpInside)
searchButton.titleEdgeInsets = .init(top: 0, left: 16, bottom: 0, right: 16)
view.addSubview(searchButton)
searchButton.snp.makeConstraints { (make) in
make.centerX.equalToSuperview()
make.top.equalToSuperview().inset(100)
}
}
}
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "1.000",
"green" : "1.000",
"red" : "1.000"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.078",
"green" : "0.075",
"red" : "0.075"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "1.000",
"green" : "1.000",
"red" : "1.000"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "1.000",
"green" : "1.000",
"red" : "1.000"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
//
// NavigationCityButton.swift
// 1Weather
//
// Created by Dmitry Stepanets on 08.02.2021.
//
import UIKit
class NavigationCityButton: UIView {
//Private
private let locationIcon = UIImageView()
private let button = SelfSizingButton()
//Public
var onButtonTouch:(() -> Void)?
init() {
super.init(frame: .zero)
prepareIcon()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
private extension NavigationCityButton {
func prepareIcon() {
locationIcon.backgroundColor = .red
addSubview(locationIcon)
locationIcon.snp.makeConstraints { (make) in
make.left.equalToSuperview().inset(16)
make.centerY.equalToSuperview()
make.size.equalTo(CGSize(width: 18, height: 18))
}
}
func prepareButton() {
button.setTitle("New York", for: .normal)
button
}
}
......@@ -9,7 +9,6 @@ import UIKit
public struct ThemeManager {
struct Colors {
static let
static let locationBlue = UIColor(hex: 0x1071F0)
static let temperatureLabelBG = UIColor(hex: 0x5F5F5F)
static let citySelected = UIColor(hex: 0x599A0E)
......
......@@ -13,11 +13,11 @@ struct DefaultTheme: ThemeProtocol {
}
var navigationBarBackgroundColor: UIColor {
return UIColor(hex: 0x17181A)
return UIColor(named: "primary_color") ?? .red
}
var navigationTintColor: UIColor {
return .white
return UIColor(named: "primary_font") ?? .red
}
var tabBarTintColor: UIColor {
......
//
// ForecastViewController.swift
// 1Weather
//
// Created by Dmitry Stepanets on 08.02.2021.
//
import UIKit
class ForecastViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .blue
}
}
......@@ -127,8 +127,23 @@ class LocationsViewModel {
place.country = hit.object.country ?? "" // ?? "" part is from the Android code, may be not needed on iOS
place.countryCode = hit.object.countryCode?.rawValue.uppercased() ?? "" // ?? "" part is from the Android code, may be not needed on iOS
if let geolocation = hit.geolocation {
place.latitude = "\(geolocation.latitude)"
place.longitude = "\(geolocation.longitude)"
switch geolocation {
case .single(let location):
place.latitude = "\(location.latitude)"
place.longitude = "\(location.longitude)"
case .list(let locations):
if let firstLat = locations.first?.latitude,
let firstLong = locations.first?.longitude
{
place.latitude = "\(firstLat)"
place.longitude = "\(firstLong)"
}
else {
place.latitude = ""
place.longitude = ""
}
}
}
else {
place.latitude = ""
......
//
// TodayViewController.swift
// 1Weather
//
// Created by Dmitry Stepanets on 08.02.2021.
//
import UIKit
class TodayViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor(named: "primary_color")
prepareNavigationBar()
}
}
private extension TodayViewController {
func prepareNavigationBar() {
let cityButton = SelfSizingButton()
cityButton.tit
}
}
//
// ViewController.swift
// 1Weather
//
// Created by Dmitry Stepanets on 12.11.2020.
//
import UIKit
import SnapKit
class ViewController: UIViewController {
let tableView = UITableView()
override func viewDidLoad() {
super.viewDidLoad()
prepareTableView()
}
}
private extension ViewController {
func prepareTableView() {
tableView.register(CurrentForecastCell.self, forCellReuseIdentifier: CurrentForecastCell.kIdentifier)
tableView.register(CurrentForecastDetailsCell.self, forCellReuseIdentifier: CurrentForecastDetailsCell.kIdentifier)
tableView.estimatedRowHeight = 160
tableView.backgroundColor = .clear
tableView.rowHeight = UITableView.automaticDimension
tableView.delegate = self
tableView.dataSource = self
tableView.backgroundColor = .blue
view.addSubview(tableView)
tableView.snp.makeConstraints { (make) in
make.edges.equalToSuperview()
}
}
}
extension ViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 2
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
switch indexPath.row {
case 0:
let cell = tableView.dequeueReusableCell(withIdentifier: CurrentForecastCell.kIdentifier, for: indexPath)
return cell
case 1:
let cell = tableView.dequeueReusableCell(withIdentifier: CurrentForecastDetailsCell.kIdentifier, for: indexPath)
return cell
default:
return UITableViewCell()
}
}
}
......@@ -7,5 +7,6 @@ target '1Weather' do
# Pods for 1Weather
pod 'SnapKit'
pod 'AlgoliaSearchClient', '~> 8.2'
pod 'synth-ios'
pod 'AlgoliaSearchClient'
end
PODS:
- AlgoliaSearchClient (8.3.0):
- AlgoliaSearchClient (8.6.0):
- Logging
- Logging (1.4.0)
- SnapKit (5.0.1)
- synth-ios (1.0.0)
DEPENDENCIES:
- AlgoliaSearchClient (~> 8.2)
- AlgoliaSearchClient
- SnapKit
- synth-ios
SPEC REPOS:
trunk:
- AlgoliaSearchClient
- Logging
- SnapKit
- synth-ios
SPEC CHECKSUMS:
AlgoliaSearchClient: 3a2f00249fc9013cd4fc0a2d27f8ac56f5219db6
AlgoliaSearchClient: 36cb507cbf100def2d028f479eb8f194cb37c604
Logging: beeb016c9c80cf77042d62e83495816847ef108b
SnapKit: 97b92857e3df3a0c71833cce143274bf6ef8e5eb
synth-ios: b91d9176ecc7ff791d94df9aedafbf5a8c00f965
PODFILE CHECKSUM: 04d64ceaca7895595534185b5c15c69d95ca82cf
PODFILE CHECKSUM: e1d8b684b1d845d76a34a4f0b3756d265fdecbf9
COCOAPODS: 1.10.0
COCOAPODS: 1.10.1
......@@ -13,7 +13,7 @@
<img src="http://img.shields.io/cocoapods/v/AlgoliaSearchClient.svg?style=flat"></img>
</a>
<a href="https://cocoapods.org/pods/AlgoliaSearchClient">
<img src="http://img.shields.io/cocoapods/p/AlgoliaSearchClient.svg?style=flat"></img>
<img src="https://img.shields.io/badge/platform-macOS%20%7C%20iOS%20%7C%20tvOS%20%7C%20watchOS%20%7C%20Linux%20-lightgray.svg?style=flat"></img>
</a>
<a href="https://github.com/Carthage/Carthage">
<img src="https://img.shields.io/badge/Carthage-compatible-brightgreen.svg"></img>
......@@ -60,9 +60,9 @@ If you're a framework author and use Swift API Client as a dependency, update yo
```swift
let package = Package(
// 8.2.0 ..< 9.0.0
// 8.5.0 ..< 9.0.0
dependencies: [
.package(url: "https://github.com/algolia/algoliasearch-client-swift", from: "8.2.0")
.package(url: "https://github.com/algolia/algoliasearch-client-swift", from: "8.5.0")
],
// ...
)
......@@ -77,7 +77,7 @@ Add `import AlgoliaSearchClient` to your source files.
To install Algolia Swift Client, simply add the following line to your Podfile:
```ruby
pod 'AlgoliaSearchClient', '~> 8.2'
pod 'AlgoliaSearchClient', '~> 8.5'
# pod 'InstantSearchClient', '~> 6.0'` // Swift 4.2
# pod 'InstantSearchClient', '~> 5.0'` // Swift 4.1
```
......@@ -94,7 +94,7 @@ $ pod update
- To install InstantSearch, simply add the following line to your Cartfile:
```ruby
github "algolia/algoliasearch-client-swift" ~> 8.2
github "algolia/algoliasearch-client-swift" ~> 8.5
# github "algolia/algoliasearch-client-swift" ~> 6.0.0` // Swift 4.2
# github "algolia/algoliasearch-client-swift" ~> 5.0.0` // Swift 4.1
```
......@@ -196,6 +196,10 @@ For full documentation, visit the [Algolia Swift API Client's documentation](htt
You can find code samples in the [Algolia's API Clients playground](https://github.com/algolia/api-clients-playground/tree/master/swift).
## Use the Dockerfile
If you want to contribute to this project without installing all its dependencies, you can use our Docker image. Please check our [dedicated guide](DOCKER_README.MD) to learn more.
## 📄 License
Algolia Swift API Client is an open-sourced software licensed under the [MIT license](LICENSE).
......
......@@ -9,10 +9,9 @@ import Foundation
class WaitTask: AsyncOperation, ResultContainer {
typealias TaskIDProvider = () -> TaskID?
typealias TaskStatusService = (RequestOptions?, @escaping ResultCallback<TaskInfo>) -> Void
let index: Index
let taskIDProvider: TaskIDProvider
let taskStatusService: TaskStatusService
let requestOptions: RequestOptions?
let timeout: TimeInterval?
private var launchDate: Date?
......@@ -32,25 +31,11 @@ class WaitTask: AsyncOperation, ResultContainer {
return Date().timeIntervalSince(launchDate) >= timeout
}
init(index: Index,
taskIDProvider: @autoclosure @escaping TaskIDProvider,
init(taskStatusService: @escaping TaskStatusService,
timeout: TimeInterval? = nil,
requestOptions: RequestOptions?,
completion: @escaping ResultCallback<TaskStatus>) {
self.index = index
self.taskIDProvider = taskIDProvider
self.timeout = timeout
self.requestOptions = requestOptions
self.completion = completion
}
init(index: Index,
taskID: TaskID,
timeout: TimeInterval? = nil,
requestOptions: RequestOptions?,
completion: @escaping ResultCallback<TaskStatus>) {
self.index = index
self.taskIDProvider = { return taskID }
self.taskStatusService = taskStatusService
self.timeout = timeout
self.requestOptions = requestOptions
self.completion = completion
......@@ -70,12 +55,7 @@ class WaitTask: AsyncOperation, ResultContainer {
return
}
guard let taskID = taskIDProvider() else {
result = .failure(Error.missingTaskID)
return
}
index.taskStatus(for: taskID, requestOptions: requestOptions) { [weak self] result in
taskStatusService(requestOptions) { [weak self] result in
guard let request = self else { return }
switch result {
......@@ -97,7 +77,6 @@ class WaitTask: AsyncOperation, ResultContainer {
enum Error: Swift.Error {
case timeout
case missingTaskID
}
}
......@@ -105,12 +84,22 @@ class WaitTask: AsyncOperation, ResultContainer {
extension WaitTask {
convenience init(index: Index,
task: Task,
taskID: TaskID,
timeout: TimeInterval? = nil,
requestOptions: RequestOptions?,
completion: @escaping ResultCallback<TaskStatus>) {
self.init(taskStatusService: { requestOptions, completion in index.taskStatus(for: taskID, requestOptions: requestOptions, completion: completion) },
timeout: timeout,
requestOptions: requestOptions,
completion: completion)
}
convenience init(client: Client,
taskID: AppTaskID,
timeout: TimeInterval? = nil,
requestOptions: RequestOptions?,
completion: @escaping ResultCallback<TaskStatus>) {
self.init(index: index,
taskID: task.taskID,
self.init(taskStatusService: { requestOptions, completion in client.taskStatus(for: taskID, requestOptions: requestOptions, completion: completion) },
timeout: timeout,
requestOptions: requestOptions,
completion: completion)
......
......@@ -84,8 +84,7 @@ public struct AccountClient {
waitSettings
].map(\.task) + waitObjects.batchesResponse.tasks
let waitService = WaitService(taskIndices: tasks.map { (destination, $0.taskID) })
return WaitableWrapper(wrapped: tasks, waitService: waitService)
return WaitableWrapper(tasks: tasks, index: destination)
}
}
......
......@@ -108,9 +108,6 @@ public extension InsightsClient {
- Returns: JSON object
*/
@discardableResult func sendEvents(_ events: [InsightsEvent], requestOptions: RequestOptions? = nil) throws -> Empty {
var updatedRequestOptions = requestOptions ?? .init()
updatedRequestOptions.setHeader("application/json", forKey: .contentType)
let requestOptions = updatedRequestOptions
let command = Command.Insights.SendEvents(events: events, requestOptions: requestOptions)
return try execute(command)
}
......
......@@ -8,9 +8,9 @@
import Foundation
public extension SearchClient {
// MARK: - List clusters
/**
List the Cluster available in a multi-clusters setup for a single ApplicationID.
......@@ -23,7 +23,7 @@ public extension SearchClient {
let command = Command.MultiCluster.ListClusters(requestOptions: requestOptions)
return execute(command, completion: completion)
}
/**
List the Cluster available in a multi-clusters setup for a single ApplicationID.
......@@ -34,9 +34,9 @@ public extension SearchClient {
let command = Command.MultiCluster.ListClusters(requestOptions: requestOptions)
return try execute(command)
}
// MARK: - List User IDs
/**
List the UserID assigned to a multi-clusters ApplicationID.
......@@ -55,7 +55,7 @@ public extension SearchClient {
let command = Command.MultiCluster.User.GetList(page: page, hitsPerPage: hitsPerPage, requestOptions: requestOptions)
return execute(command, completion: completion)
}
/**
List the UserID assigned to a multi-clusters ApplicationID.
......@@ -72,7 +72,7 @@ public extension SearchClient {
let command = Command.MultiCluster.User.GetList(page: page, hitsPerPage: hitsPerPage, requestOptions: requestOptions)
return try execute(command)
}
// MARK: - Assign UserID
/**
Assign or Move a UserID to a cluster.
......@@ -93,7 +93,7 @@ public extension SearchClient {
let command = Command.MultiCluster.User.Assign(userID: userID, clusterName: clusterName, requestOptions: requestOptions)
return execute(command, completion: completion)
}
/**
Assign or Move a UserID to a cluster.
......@@ -111,9 +111,9 @@ public extension SearchClient {
let command = Command.MultiCluster.User.Assign(userID: userID, clusterName: clusterName, requestOptions: requestOptions)
return try execute(command)
}
// MARK: - Assign UserIDs
/**
Assign or move UserIDs to a ClusterName.
......@@ -132,7 +132,7 @@ public extension SearchClient {
let command = Command.MultiCluster.User.BatchAssign(userIDs: userIDs, clusterName: clusterName, requestOptions: requestOptions)
return execute(command, completion: completion)
}
/**
Assign or move UserIDs to a ClusterName.
......@@ -149,9 +149,9 @@ public extension SearchClient {
let command = Command.MultiCluster.User.BatchAssign(userIDs: userIDs, clusterName: clusterName, requestOptions: requestOptions)
return try execute(command)
}
// MARK: - Get UserID
/**
Returns the UserID data stored in the mapping.
......@@ -168,7 +168,7 @@ public extension SearchClient {
let command = Command.MultiCluster.User.Get(userID: userID, requestOptions: requestOptions)
return execute(command, completion: completion)
}
/**
Returns the UserID data stored in the mapping.
......@@ -183,9 +183,9 @@ public extension SearchClient {
let command = Command.MultiCluster.User.Get(userID: userID, requestOptions: requestOptions)
return try execute(command)
}
// MARK: - Get top UserID
/**
Get the top 10 ResponseUserID with the highest number of records per cluster.
The data returned will usually be a few seconds behind real-time, because userID usage may take up to a few seconds to propagate to the different clusters.
......@@ -199,7 +199,7 @@ public extension SearchClient {
let command = Command.MultiCluster.User.GetTop(requestOptions: requestOptions)
return execute(command, completion: completion)
}
/**
Get the top 10 ResponseUserID with the highest number of records per cluster.
The data returned will usually be a few seconds behind real-time, because userID usage may take up to a few seconds to propagate to the different clusters.
......@@ -211,9 +211,9 @@ public extension SearchClient {
let command = Command.MultiCluster.User.GetTop(requestOptions: requestOptions)
return try execute(command)
}
// MARK: - Remove UserID
/**
Remove a UserID and its associated data from the multi-clusters.
......@@ -230,7 +230,7 @@ public extension SearchClient {
let command = Command.MultiCluster.User.Remove(userID: userID, requestOptions: requestOptions)
return execute(command, completion: completion)
}
/**
Remove a UserID and its associated data from the multi-clusters.
......@@ -245,9 +245,9 @@ public extension SearchClient {
let command = Command.MultiCluster.User.Remove(userID: userID, requestOptions: requestOptions)
return try execute(command)
}
// MARK: - Search UserID
/**
Search for UserID.
......@@ -264,7 +264,7 @@ public extension SearchClient {
let command = Command.MultiCluster.User.Search(userIDQuery: query, requestOptions: requestOptions)
return execute(command, completion: completion)
}
/**
Search for UserID.
......@@ -279,9 +279,9 @@ public extension SearchClient {
let command = Command.MultiCluster.User.Search(userIDQuery: query, requestOptions: requestOptions)
return try execute(command)
}
// MARK: - Has pending mapping
/**
- Parameter retrieveMappings: If set to true, retrieves HasPendingMappingResponse.clusters.
- Parameter requestOptions: Configure request locally with RequestOptions
......@@ -294,7 +294,7 @@ public extension SearchClient {
let command = Command.MultiCluster.HasPendingMapping(retrieveMapping: retrieveMappings, requestOptions: requestOptions)
return execute(command, completion: completion)
}
/**
- Parameter retrieveMappings: If set to true, retrieves HasPendingMappingResponse.clusters.
- Parameter requestOptions: Configure request locally with RequestOptions
......@@ -305,5 +305,5 @@ public extension SearchClient {
let command = Command.MultiCluster.HasPendingMapping(retrieveMapping: retrieveMappings, requestOptions: requestOptions)
return try execute(command)
}
}
//
// SearchClient+Wait.swift
//
//
// Created by Vladislav Fitc on 22/01/2021.
//
import Foundation
public extension Client {
// MARK: - Task status
/**
Check the current TaskStatus of a given Task.
- parameter taskID: of the indexing [Task].
- parameter requestOptions: Configure request locally with [RequestOptions]
*/
@discardableResult func taskStatus(for taskID: AppTaskID,
requestOptions: RequestOptions? = nil,
completion: @escaping ResultCallback<TaskInfo>) -> Operation & TransportTask {
let command = Command.Advanced.TaskStatus(taskID: taskID, requestOptions: requestOptions)
return execute(command, completion: completion)
}
/**
Check the current TaskStatus of a given Task.
- parameter taskID: of the indexing [Task].
- parameter requestOptions: Configure request locally with [RequestOptions]
*/
@discardableResult func taskStatus(for taskID: AppTaskID,
requestOptions: RequestOptions? = nil) throws -> TaskInfo {
let command = Command.Advanced.TaskStatus(taskID: taskID, requestOptions: requestOptions)
return try execute(command)
}
// MARK: - Wait task
/**
Wait for a Task to complete before executing the next line of code, to synchronize index updates.
All write operations in Algolia are asynchronous by design.
It means that when you add or update an object to your index, our servers will reply to your request with
a TaskID as soon as they understood the write operation.
The actual insert and indexing will be done after replying to your code.
You can wait for a task to complete by using the TaskID and this method.
- parameter taskID: of the indexing task to wait for.
- parameter requestOptions: Configure request locally with RequestOptions
*/
@discardableResult func waitTask(withID taskID: AppTaskID,
timeout: TimeInterval? = nil,
requestOptions: RequestOptions? = nil,
completion: @escaping ResultCallback<TaskStatus>) -> Operation {
let task = WaitTask(client: self,
taskID: taskID,
timeout: timeout,
requestOptions: requestOptions,
completion: completion)
return launch(task)
}
/**
Wait for a Task to complete before executing the next line of code, to synchronize index updates.
All write operations in Algolia are asynchronous by design.
It means that when you add or update an object to your index, our servers will reply to your request with
a TaskID as soon as they understood the write operation.
The actual insert and indexing will be done after replying to your code.
You can wait for a task to complete by using the TaskID and this method.
- parameter taskID: of the indexing task to wait for.
- parameter requestOptions: Configure request locally with RequestOptions
*/
@discardableResult func waitTask(withID taskID: AppTaskID,
timeout: TimeInterval? = nil,
requestOptions: RequestOptions? = nil) throws -> TaskStatus {
let task = WaitTask(client: self,
taskID: taskID,
timeout: timeout,
requestOptions: requestOptions,
completion: { _ in })
return try launch(task)
}
}
......@@ -77,6 +77,18 @@ extension SearchClient: TransportContainer {}
extension SearchClient {
func execute<Output: Codable & AppTask>(_ command: AlgoliaCommand, completion: @escaping ResultAppTaskCallback<Output>) -> Operation & TransportTask {
transport.execute(command, transform: WaitableWrapper.wrap(with: self), completion: completion)
}
func execute<Output: Codable & AppTask>(_ command: AlgoliaCommand) throws -> WaitableWrapper<Output> {
try transport.execute(command, transform: WaitableWrapper.wrap(with: self))
}
}
extension SearchClient {
@discardableResult func launch<O: Operation>(_ operation: O) -> O {
return operationLauncher.launch(operation)
}
......
......@@ -25,6 +25,13 @@ extension Command {
let path = .indexesV1 >>> .index(indexName) >>> IndexCompletion.task(for: taskID)
urlRequest = .init(method: .get, path: path, requestOptions: self.requestOptions)
}
init(taskID: AppTaskID, requestOptions: RequestOptions?) {
self.requestOptions = requestOptions
let path = .task >>> TaskCompletion.task(withID: taskID)
urlRequest = .init(method: .get, path: path, requestOptions: self.requestOptions)
}
}
struct GetLogs: AlgoliaCommand {
......
//
// Command+Answers.swift
//
//
// Created by Vladislav Fitc on 19/11/2020.
//
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
extension Command {
enum Answers {
struct Find: AlgoliaCommand {
let callType: CallType = .read
let urlRequest: URLRequest
let requestOptions: RequestOptions?
init(indexName: IndexName,
query: AnswersQuery,
requestOptions: RequestOptions?) {
self.requestOptions = requestOptions
let path: IndexCompletion = .answers >>> .index(indexName) >>> .prediction
urlRequest = .init(method: .post, path: path, body: query.httpBody, requestOptions: self.requestOptions)
}
}
}
}
//
// Command+Dictionaries.swift
//
//
// Created by Vladislav Fitc on 20/01/2021.
//
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
extension Command {
enum Dictionaries {
struct Batch: AlgoliaCommand {
let callType: CallType = .write
let urlRequest: URLRequest
let requestOptions: RequestOptions?
init<D: CustomDictionary>(dictionary: D.Type,
requests: [DictionaryRequest<D.Entry>],
clearExistingDictionaryEntries: Bool,
requestOptions: RequestOptions?) where D.Entry: Encodable {
let path = .dictionaries >>> .dictionaryName(D.name) >>> DictionaryCompletion.batch
let body = Payload(requests: requests, clearExistingDictionaryEntries: clearExistingDictionaryEntries).httpBody
self.urlRequest = URLRequest(method: .post, path: path, body: body, requestOptions: requestOptions)
self.requestOptions = requestOptions
}
// swiftlint:disable:next nesting
struct Payload<E: DictionaryEntry & Encodable>: Encodable {
let requests: [DictionaryRequest<E>]
let clearExistingDictionaryEntries: Bool
}
}
struct Search: AlgoliaCommand {
let callType: CallType = .read
let urlRequest: URLRequest
let requestOptions: RequestOptions?
init(dictionaryName: DictionaryName,
query: DictionaryQuery,
requestOptions: RequestOptions?) {
self.requestOptions = requestOptions
let path = .dictionaries >>> .dictionaryName(dictionaryName) >>> DictionaryCompletion.search
let body = query.httpBody
self.urlRequest = .init(method: .post, path: path, body: body, requestOptions: self.requestOptions)
}
init<D: CustomDictionary>(dictionary: D.Type,
query: DictionaryQuery,
requestOptions: RequestOptions?) {
self.requestOptions = requestOptions
let path = .dictionaries >>> .dictionaryName(D.name) >>> DictionaryCompletion.search
let body = query.httpBody
self.urlRequest = .init(method: .post, path: path, body: body, requestOptions: self.requestOptions)
}
}
struct GetSettings: AlgoliaCommand {
let callType: CallType = .read
let urlRequest: URLRequest
let requestOptions: RequestOptions?
init(requestOptions: RequestOptions?) {
self.requestOptions = requestOptions
let path = .dictionaries >>> .common >>> DictionaryCompletion.settings
self.urlRequest = .init(method: .get, path: path, requestOptions: self.requestOptions)
}
}
struct SetSettings: AlgoliaCommand {
let callType: CallType = .write
let urlRequest: URLRequest
let requestOptions: RequestOptions?
init(settings: DictionarySettings,
requestOptions: RequestOptions?) {
self.requestOptions = requestOptions
let path = .dictionaries >>> .common >>> DictionaryCompletion.settings
self.urlRequest = .init(method: .put, path: path, body: settings.httpBody, requestOptions: self.requestOptions)
}
}
struct LanguagesList: AlgoliaCommand {
let callType: CallType = .read
let urlRequest: URLRequest
let requestOptions: RequestOptions?
init(requestOptions: RequestOptions?) {
self.requestOptions = requestOptions
let path = .dictionaries >>> .common >>> DictionaryCompletion.languages
self.urlRequest = .init(method: .get, path: path, requestOptions: self.requestOptions)
}
}
}
}
......@@ -22,6 +22,8 @@ extension Command {
init(events: [InsightsEvent], requestOptions: RequestOptions?) {
let body = EventsWrapper(events)
var requestOptions = requestOptions.unwrapOrCreate()
requestOptions.setHeader("application/json", forKey: .contentType)
self.requestOptions = requestOptions
self.urlRequest = .init(method: .post, path: Path.eventsV1, body: body.httpBody, requestOptions: self.requestOptions)
}
......
//
// AssertionTestHelper.swift
//
//
// Created by Vladislav Fitc on 20/11/2020.
//
import Foundation
/// Our custom drop-in replacement `assertionFailure`.
///
/// This will call Swift's `assertionFailure` by default (and terminate the program).
/// But it can be changed at runtime to be tested instead of terminating.
func assertionFailure(_ message: @autoclosure () -> String = String(), file: StaticString = #file, line: UInt = #line) {
assertionClosure(message(), file, line)
}
/// The actual function called by our custom `precondition`.
var assertionClosure: (String, StaticString, UInt) -> Void = defaultAssertionClosure
let defaultAssertionClosure = { Swift.assertionFailure($0, file: $1, line: $2) }
......@@ -12,13 +12,13 @@ import Foundation
occasionally providing bool values in the form of String
*/
struct BoolContainer: RawRepresentable, Codable {
let rawValue: Bool
init(rawValue: Bool) {
self.rawValue = rawValue
}
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let boolValue = try? container.decode(Bool.self) {
......@@ -31,5 +31,5 @@ struct BoolContainer: RawRepresentable, Codable {
}
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Value cannot be decoded neither to Bool nor to String representing Bool value")
}
}
......@@ -14,7 +14,7 @@ import CommonCrypto
#endif
extension String {
func hmac256(withKey key: String) -> String {
#if os(Linux)
let key = SymmetricKey(data: Data(key.utf8))
......
......@@ -9,4 +9,5 @@ import Foundation
public typealias ResultCallback<T> = (Result<T, Error>) -> Void
public typealias ResultTaskCallback<T: Task & Codable> = (Result<WaitableWrapper<T>, Error>) -> Void
public typealias ResultAppTaskCallback<T: AppTask & Codable> = (Result<WaitableWrapper<T>, Error>) -> Void
public typealias ResultBatchesCallback = (Result<WaitableWrapper<BatchesResponse>, Error>) -> Void
......@@ -8,8 +8,8 @@
import Foundation
extension String {
func wrappedInQuotes() -> String { "\"\(self)\"" }
func wrappedInBrackets() -> String { "[\(self)]" }
}
// This is generated file. Don't modify it manually.
public extension Version { static let current: Version = .init(major: 8, minor: 3, patch: 0, prereleaseIdentifier: nil) }
public extension Version { static let current: Version = .init(major: 8, minor: 6, patch: 0, prereleaseIdentifier: nil) }
//
// TaskWaitable.swift
//
//
// Created by Vladislav Fitc on 25/01/2021.
//
import Foundation
protocol TaskWaitable {
func waitTask(withID taskID: TaskID, timeout: TimeInterval?, requestOptions: RequestOptions?, completion: @escaping ResultCallback<TaskStatus>) -> Operation
func waitTask(withID taskID: TaskID, timeout: TimeInterval?, requestOptions: RequestOptions?) throws -> TaskStatus
}
//
// WaitService.swift
//
//
// Created by Vladislav Fitc on 29/04/2020.
//
import Foundation
struct WaitService {
let taskIndices: [(Index, TaskID)]
init<T: Task>(index: Index, task: T) {
self.taskIndices = [(index, task.taskID)]
}
init(taskIndices: [(Index, TaskID)]) {
self.taskIndices = taskIndices
}
init(client: SearchClient, taskIndex: [IndexedTask]) {
self.taskIndices = taskIndex.map { (client.index(withName: $0.indexName), $0.taskID) }
}
}
extension WaitService: AnyWaitable {
func wait(timeout: TimeInterval?, requestOptions: RequestOptions?) throws {
for (index, taskID) in taskIndices {
try index.waitTask(withID: taskID, timeout: timeout, requestOptions: requestOptions)
}
}
func wait(timeout: TimeInterval?, requestOptions: RequestOptions? = nil, completion: @escaping (Result<Empty, Error>) -> Void) {
let dispatchGroup = DispatchGroup()
var outputError: Error?
for (index, taskID) in taskIndices {
dispatchGroup.enter()
index.waitTask(withID: taskID,
timeout: timeout,
requestOptions: requestOptions) { result in
switch result {
case .success:
break
case .failure(let error):
outputError = error
}
dispatchGroup.leave()
}
}
dispatchGroup.notify(queue: .global(qos: .userInteractive)) {
let result: Result<Empty, Error>
if let error = outputError {
result = .failure(error)
} else {
result = .success(.empty)
}
completion(result)
}
}
}
//
// Waitable.swift
//
//
// Created by Vladislav Fitc on 01/02/2021.
//
import Foundation
struct Waitable: AnyWaitable {
let asyncCall: (TimeInterval?, RequestOptions?, @escaping ResultCallback<Empty>) -> Void
let syncCall: (TimeInterval?, RequestOptions?) throws -> Void
init(index: Index, taskID: TaskID) {
asyncCall = { timeout, requestOptions, completion in index.waitTask(withID: taskID, timeout: timeout, requestOptions: requestOptions, completion: { result in completion(result.map { _ in .empty }) }) }
syncCall = { try index.waitTask(withID: taskID, timeout: $0, requestOptions: $1) }
}
init(client: Client, task: IndexedTask) {
self.init(index: client.index(withName: task.indexName), taskID: task.taskID)
}
init(client: SearchClient, taskID: AppTaskID) {
asyncCall = { timeout, requestOptions, completion in client.waitTask(withID: taskID, timeout: timeout, requestOptions: requestOptions, completion: { result in completion(result.map { _ in .empty }) }) }
syncCall = { try client.waitTask(withID: taskID, timeout: $0, requestOptions: $1) }
}
public func wait(timeout: TimeInterval? = nil,
requestOptions: RequestOptions? = nil) throws {
try syncCall(timeout, requestOptions)
}
public func wait(timeout: TimeInterval? = nil,
requestOptions: RequestOptions? = nil,
completion: @escaping (Result<Empty, Swift.Error>) -> Void) {
asyncCall(timeout, requestOptions, completion)
}
}
......@@ -10,11 +10,11 @@ import Foundation
public struct WaitableWrapper<T> {
public let wrapped: T
let waitService: WaitService
let tasksToWait: [Waitable]
init(wrapped: T, waitService: WaitService) {
init(wrapped: T, tasksToWait: [Waitable]) {
self.wrapped = wrapped
self.waitService = waitService
self.tasksToWait = tasksToWait
}
}
......@@ -27,7 +27,7 @@ extension WaitableWrapper where T: Task {
init(task: T, index: Index) {
self.wrapped = task
self.waitService = .init(index: index, task: task)
self.tasksToWait = [.init(index: index, taskID: task.taskID)]
}
static func wrap(with index: Index) -> (T) -> WaitableWrapper<T> {
......@@ -38,6 +38,38 @@ extension WaitableWrapper where T: Task {
}
extension WaitableWrapper where T == [Task] {
public var tasks: T {
return wrapped
}
init(tasks: [Task], index: Index) {
self.wrapped = tasks
self.tasksToWait = tasks.map { Waitable(index: index, taskID: $0.taskID) }
}
}
extension WaitableWrapper where T: AppTask {
public var task: T {
return wrapped
}
init(task: T, client: SearchClient) {
self.wrapped = task
self.tasksToWait = [.init(client: client, taskID: task.taskID)]
}
static func wrap(with client: SearchClient) -> (T) -> WaitableWrapper<T> {
return { task in
return WaitableWrapper(task: task, client: client)
}
}
}
extension WaitableWrapper where T: Task & IndexNameContainer {
static func wrap(credentials: Credentials) -> (T) -> WaitableWrapper<T> {
......@@ -57,13 +89,12 @@ extension WaitableWrapper where T == BatchesResponse {
init(batchesResponse: T, client: SearchClient) {
self.wrapped = batchesResponse
self.waitService = .init(client: client, taskIndex: batchesResponse.tasks)
self.tasksToWait = batchesResponse.tasks.map { Waitable(index: client.index(withName: $0.indexName), taskID: $0.taskID) }
}
init(batchesResponse: T, index: Index) {
self.wrapped = batchesResponse
let taskIndices = batchesResponse.tasks.map { (index, $0.taskID) }
self.waitService = .init(taskIndices: taskIndices)
self.tasksToWait = batchesResponse.tasks.map { Waitable(index: index, taskID: $0.taskID) }
}
}
......@@ -72,13 +103,32 @@ extension WaitableWrapper: AnyWaitable {
public func wait(timeout: TimeInterval? = nil,
requestOptions: RequestOptions? = nil) throws {
try waitService.wait(timeout: timeout, requestOptions: requestOptions)
for waiter in tasksToWait {
try waiter.wait(timeout: timeout, requestOptions: requestOptions)
}
}
public func wait(timeout: TimeInterval? = nil,
requestOptions: RequestOptions? = nil,
completion: @escaping (Result<Empty, Swift.Error>) -> Void) {
waitService.wait(timeout: timeout, requestOptions: requestOptions, completion: completion)
let dispatchGroup = DispatchGroup()
var outputError: Error?
for waiter in tasksToWait {
dispatchGroup.enter()
waiter.wait(timeout: timeout,
requestOptions: requestOptions) { result in
switch result {
case .success:
break
case .failure(let error):
outputError = error
}
dispatchGroup.leave()
}
}
dispatchGroup.notify(queue: .global(qos: .userInteractive)) {
completion(outputError.flatMap { .failure($0) } ?? .success(.empty))
}
}
}
......@@ -58,6 +58,7 @@ public extension Index {
timeout: timeout,
requestOptions: requestOptions,
completion: completion)
return launch(task)
}
......@@ -84,3 +85,5 @@ public extension Index {
}
}
extension Index: TaskWaitable {}
//
// Index+Answers.swift
//
//
// Created by Vladislav Fitc on 20/11/2020.
//
import Foundation
public extension Index {
/**
Returns answers that match the query.
- Parameter query: The AnswersQuery used to search.
- Parameter requestOptions: Configure request locally with RequestOptions.
- Parameter completion: Result completion
- Returns: Launched asynchronous operation
*/
@discardableResult func findAnswers(for query: AnswersQuery,
requestOptions: RequestOptions? = nil,
completion: @escaping ResultCallback<SearchResponse>) -> Operation & TransportTask {
let command = Command.Answers.Find(indexName: name, query: query, requestOptions: requestOptions)
return execute(command, completion: completion)
}
/**
Returns answers that match the query.
- Parameter query: The AnswersQuery used to search.
- Parameter requestOptions: Configure request locally with RequestOptions.
- Returns: SearchResponse object
*/
@discardableResult func findAnswers(for query: AnswersQuery,
requestOptions: RequestOptions? = nil) throws -> SearchResponse {
let command = Command.Answers.Find(indexName: name, query: query, requestOptions: requestOptions)
return try execute(command)
}
}
......@@ -541,14 +541,10 @@ public extension Index {
}
let client = SearchClient(appID: self.applicationID, apiKey: self.apiKey)
let waitService = WaitService(client: client, taskIndex: tasks)
WaitableWrapper(wrapped: tasks, waitService: waitService).wait { result in
switch result {
case .failure(let error):
completion(.failure(error))
case .success:
completion(.success(tasks))
}
let tasksToWait = tasks.map { Waitable(client: client, task: $0) }
WaitableWrapper(wrapped: tasks, tasksToWait: tasksToWait).wait { result in
completion(result.map { _ in tasks})
}
}
}
......
......@@ -112,7 +112,7 @@ public extension Index {
let command = Command.Synonym.SaveList(indexName: name, synonyms: synonyms, forwardToReplicas: forwardToReplicas, clearExistingSynonyms: replaceExistingSynonyms, requestOptions: requestOptions)
return try execute(command)
}
/**
Create or update multiple synonym.
This method enables you to create or update one or more synonym in a single call.
......
......@@ -10,18 +10,18 @@ import Foundation
extension ABTestResponse {
public struct Variant: Codable {
public let indexName: IndexName
public let trafficPercentage: Int
/// Distinct click count for the variant.
public let clickCount: Int
public let clickCount: Int?
/// Distinct conversion count for the variant.
public let conversionCount: Int
public let description: String
public let conversionCount: Int?
public let indexName: IndexName
public let trafficPercentage: Int
public let description: String?
/// Conversion rate for the variant.
public let conversionRate: Double?
......
......@@ -17,7 +17,7 @@ public struct ABTestResponse {
public let clickSignificance: Double?
/// ABTest significance based on conversion data.
/// Should be > 0.95 to be considered significant (no matter which variant is winning
/// Should be > 0.95 to be considered significant (no matter which variant is winning)
public let conversionSignificance: Double?
/// Time at which the ABTest has been created.
......
//
// AnswersQuery+Language.swift
//
//
// Created by Vladislav Fitc on 20/11/2020.
//
import Foundation
extension AnswersQuery {
public enum Language: RawRepresentable, Codable {
case english
case custom(String)
public var rawValue: AlgoliaSearchClient.Language {
switch self {
case .english:
return .english
case .custom(let rawValue):
return .init(rawValue: rawValue)
}
}
public init(rawValue: AlgoliaSearchClient.Language) {
switch rawValue {
case .english:
self = .english
default:
self = .custom(rawValue.rawValue)
}
}
public init(from decoder: Decoder) throws {
let rawLanguage = try AlgoliaSearchClient.Language(from: decoder)
self.init(rawValue: rawLanguage)
}
public func encode(to encoder: Encoder) throws {
try rawValue.encode(to: encoder)
}
}
}
//
// AnswersQuery.swift
//
//
// Created by Vladislav Fitc on 19/11/2020.
//
import Foundation
public struct AnswersQuery: SearchParameters, Codable {
/// The query for which to retrieve results.
public var query: String
/// The languages in the query.
///
/// Default value: [.english]
public var queryLanguages: [Language]
/// Attributes to use for predictions.
///
/// Default value: ["*"] (all searchable attributes)
/// - Warning: All your attributesForPrediction must be part of your searchableAttributes.
public var attributesForPrediction: [Attribute]?
/// Maximum number of answers to retrieve from the Answers Engine.
///
/// Default value: 10
///
/// Cannot be greater than 1000.
public var nbHits: Int?
/// Threshold for the answers’ confidence score: only answers with extracts that score above this threshold are returned.
///
/// Default value: 0.0
public var threshold: Double?
/// Not supported by Answers
public var attributesToSnippet: [Snippet]? {
get {
return nil
}
// swiftlint:disable:next unused_setter_value
set {
assertionFailure("attributesToSnippet is not supported by answers")
}
}
/// Not supported by Answers
public var hitsPerPage: Int? {
get {
return nil
}
// swiftlint:disable:next unused_setter_value
set {
assertionFailure("hitsPerPage is not supported by answers")
}
}
/// Not supported by Answers
public var restrictSearchableAttributes: [Attribute]? {
get {
return nil
}
// swiftlint:disable:next unused_setter_value
set {
assertionFailure("restrictSearchableAttributes is not supported by answers")
}
}
internal var params: SearchParametersStorage?
public init(query: String, queryLanguages: [Language]) {
self.query = query
self.queryLanguages = queryLanguages
}
}
extension AnswersQuery: ExpressibleByStringLiteral {
public init(stringLiteral value: String) {
self.init(query: value, queryLanguages: [.english])
}
}
extension AnswersQuery: SearchParametersStorageContainer {
var searchParametersStorage: SearchParametersStorage {
get {
return params ?? .init()
}
set {
params = newValue
}
}
}
extension AnswersQuery: Builder {}
//
// AppRevision.swift
//
//
// Created by Vladislav Fitc on 01/02/2021.
//
import Foundation
public struct AppRevision: AppTask, Codable {
/// Date at which the update happened.
public let updatedAt: Date
/// The TaskID which can be used with the .waitTask method.
public let taskID: AppTaskID
}
//
// Revision.swift
//
//
// Created by Vladislav Fitc on 21/01/2021.
//
import Foundation
public struct Revision: Task, Codable {
/// Date at which the update happened.
public let updatedAt: Date
/// The TaskID which can be used with the .waitTask method.
public let taskID: TaskID
}
//
// CompoundsDictionary.swift
//
//
// Created by Vladislav Fitc on 20/01/2021.
//
import Foundation
public struct CompoundsDictionary: CustomDictionary {
public static var name: DictionaryName = .compounds
}
public extension CompoundsDictionary {
struct Entry: DictionaryEntry, Codable, Equatable {
public let objectID: ObjectID
public let language: Language
/// When decomposition is empty: adds word as a compound atom.
/// For example, atom “kino” decomposes the query “kopfkino” into “kopf” and “kino”.
/// When decomposition isn’t empty: creates a decomposition exception.
/// For example, when decomposition is set to ["hund", "hutte"], exception “hundehutte” decomposes the word into “hund” and “hutte”, discarding the linking morpheme “e”.
public let word: String
/// When empty, the key word is considered as a compound atom.
/// Otherwise, it is the decomposition of word.
public let decomposition: [String]
public init(objectID: ObjectID, language: Language, word: String, decomposition: [String]) {
self.objectID = objectID
self.language = language
self.word = word
self.decomposition = decomposition
}
}
}
//
// CustomDictionary.swift
//
//
// Created by Vladislav Fitc on 25/01/2021.
//
import Foundation
public protocol CustomDictionary {
associatedtype Entry: DictionaryEntry
static var name: DictionaryName { get }
}
//
// DictionaryEntry.swift
//
//
// Created by Vladislav Fitc on 20/01/2021.
//
import Foundation
public protocol DictionaryEntry {
/// Unique identifier of the entry to add or override.
var objectID: ObjectID { get }
/// Language supported by the dictionary.
var language: Language { get }
}
//
// DictionaryName.swift
//
//
// Created by Vladislav Fitc on 20/01/2021.
//
import Foundation
public struct DictionaryName: StringOption {
public static var stopwords: Self { .init(rawValue: #function) }
public static var plurals: Self { .init(rawValue: #function) }
public static var compounds: Self { .init(rawValue: #function) }
public let rawValue: String
public init(rawValue: String) {
self.rawValue = rawValue
}
}
//
// DictionaryQuery.swift
//
//
// Created by Vladislav Fitc on 20/01/2021.
//
import Foundation
public struct DictionaryQuery: Codable {
public var query: String
public var page: Int?
public var hitsPerPage: Int?
public var language: Language?
public init(query: String,
page: Int? = nil,
hitsPerPage: Int? = nil,
language: Language? = nil) {
self.query = query
self.page = page
self.hitsPerPage = hitsPerPage
self.language = language
}
}
extension DictionaryQuery: ExpressibleByStringInterpolation {
public init(stringLiteral value: String) {
self.init(query: value)
}
}
//
// DictionaryRequest.swift
//
//
// Created by Vladislav Fitc on 20/01/2021.
//
import Foundation
enum DictionaryRequest<Entry: DictionaryEntry> {
enum Action: String, Codable {
case addEntry
case deleteEntry
}
case addEntry(Entry)
case deleteEntry(withObjectID: ObjectID)
static func add(_ entry: Entry) -> Self {
return .addEntry(entry)
}
static func delete<D: CustomDictionary>(dictionary: D.Type, objectID: ObjectID) -> DictionaryRequest<D.Entry> {
return .deleteEntry(withObjectID: objectID)
}
enum CodingKeys: String, CodingKey {
case action
case body
}
}
extension DictionaryRequest: Encodable where Entry: Encodable {
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case .addEntry(let entry):
try container.encode(Action.addEntry, forKey: .action)
try container.encode(entry, forKey: .body)
case .deleteEntry(let objectID):
try container.encode(Action.deleteEntry, forKey: .action)
try container.encode(ObjectIDWrapper(objectID), forKey: .body)
}
}
}
extension DictionaryRequest: Decodable where Entry: Decodable {
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let action = try container.decode(Action.self, forKey: .action)
switch action {
case .addEntry:
let entry = try container.decode(Entry.self, forKey: .body)
self = .addEntry(entry)
case .deleteEntry:
let objectID = try container.decode(ObjectIDWrapper.self, forKey: .body).wrapped
self = .deleteEntry(withObjectID: objectID)
}
}
}
//
// DictionaryRevision.swift
//
//
// Created by Vladislav Fitc on 21/01/2021.
//
import Foundation
public struct DictionaryRevision: AppTask, Codable {
/// Date at which the Task to update the dictionary has been created.
public let updatedAt: Date
/// The TaskID which can be used with the .waitTask method.
public let taskID: AppTaskID
}
//
// DictionarySearchResponse.swift
//
//
// Created by Vladislav Fitc on 21/01/2021.
//
import Foundation
public struct DictionarySearchResponse<E: DictionaryEntry> {
public let hits: [E]
public let nbHits: Int
public let page: Int
public let nbPages: Int
public init(hits: [E], nbHits: Int, page: Int, nbPages: Int) {
self.hits = hits
self.nbHits = nbHits
self.page = page
self.nbPages = nbPages
}
}
extension DictionarySearchResponse: Encodable where E: Encodable {}
extension DictionarySearchResponse: Decodable where E: Decodable {}
//
// DictionarySettings.swift
//
//
// Created by Vladislav Fitc on 20/01/2021.
//
import Foundation
public struct DictionarySettings: Codable, Equatable {
public let disableStandardEntries: DisableStandardEntries?
public init(disableStandardEntries: DisableStandardEntries?) {
self.disableStandardEntries = disableStandardEntries
}
}
extension DictionarySettings {
/// Map of language supported by the dictionary to a boolean value.
/// When set to true, the standard entries for the language are disabled.
/// Changes are set for the given languages only. To re-enable standard entries, set the language to false.
public struct DisableStandardEntries: Codable, Equatable {
public let stopwords: [Language: Bool]
public init(stopwords: [Language: Bool]) {
self.stopwords = stopwords
}
enum CodingKeys: String, CodingKey {
case stopwords
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(stopwords.mapKeys(\.rawValue), forKey: .stopwords)
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
stopwords = try container.decode([String: Bool].self, forKey: .stopwords).mapKeys(Language.init(rawValue:))
}
}
}
//
// PluralsDictionary.swift
//
//
// Created by Vladislav Fitc on 20/01/2021.
//
import Foundation
public struct PluralsDictionary: CustomDictionary {
public static var name: DictionaryName = .plurals
}
public extension PluralsDictionary {
struct Entry: DictionaryEntry, Codable, Equatable {
public let objectID: ObjectID
public let language: Language
/// List of word declensions. The entry overrides existing entries when any of these words are defined in the standard dictionary provided by Algolia.
public let words: [String]
public init(objectID: ObjectID, language: Language, words: [String]) {
self.objectID = objectID
self.language = language
self.words = words
}
}
}
//
// StopwordsDictionary.swift
//
//
// Created by Vladislav Fitc on 20/01/2021.
//
import Foundation
public struct StopwordsDictionary: CustomDictionary {
public static var name: DictionaryName = .stopwords
}
extension StopwordsDictionary {
public struct Entry: DictionaryEntry, Codable, Equatable {
public enum State: String, Codable {
case enabled
case disabled
}
public let objectID: ObjectID
public let language: Language
/// The stop word being added or modified. When `word` already exists in the standard dictionary provided by Algolia, the entry can be overridden by the one provided by the user.
public let word: String
/// The state of the entry
public let state: State?
public init(objectID: ObjectID, language: Language, word: String, state: State) {
self.objectID = objectID
self.language = language
self.word = word
self.state = state
}
}
}
......@@ -16,7 +16,7 @@ struct Path: PathComponent {
get {
return nil
}
// swiftlint:disable:next unused_setter_value
set {
}
}
......@@ -33,8 +33,9 @@ struct Path: PathComponent {
static var logs: Self { .init("/1/logs") }
static var strategies: Self { .init("/1/strategies") }
static var places: Self { .init("/1/places") }
static var task: Self { .init("/task") }
static var answers: Self { .init("/1/answers") }
static var dictionaries: Self { .init("/1/dictionaries") }
static var task: Self { .init("/1/task") }
}
struct IndexRoute: PathComponent {
......@@ -51,6 +52,18 @@ struct IndexRoute: PathComponent {
}
struct TaskCompletion: PathComponent {
var parent: Path?
let rawValue: String
private init(_ rawValue: String) { self.rawValue = rawValue }
static func task(withID taskID: AppTaskID) -> Self { .init(taskID.rawValue) }
}
struct IndexCompletion: PathComponent {
var parent: IndexRoute?
......@@ -71,6 +84,36 @@ struct IndexCompletion: PathComponent {
static func task(for taskID: TaskID) -> Self { .init("task/\(taskID.rawValue)") }
static var rules: Self { .init(#function) }
static var synonyms: Self { .init(#function) }
static var prediction: Self { .init(#function) }
}
struct DictionaryRoute: PathComponent {
var parent: Path?
let rawValue: String
private init(_ rawValue: String) { self.rawValue = rawValue }
static func dictionaryName(_ dictionaryName: DictionaryName) -> Self { .init(dictionaryName.rawValue) }
static var common: Self { .init("*") }
}
struct DictionaryCompletion: PathComponent {
var parent: DictionaryRoute?
let rawValue: String
private init(_ rawValue: String) { self.rawValue = rawValue }
static var batch: Self { .init(#function) }
static var search: Self { .init(#function) }
static var settings: Self { .init(#function) }
static var languages: Self { .init(#function) }
}
......
......@@ -27,7 +27,7 @@ extension RootPath {
get {
return nil
}
// swiftlint:disable:next unused_setter_value
set { }
}
......@@ -43,6 +43,7 @@ extension Never: PathComponent {
get {
return nil
}
// swiftlint:disable:next unused_setter_value
set {
}
}
......
......@@ -40,10 +40,10 @@ extension Rule.Alternatives: ExpressibleByBooleanLiteral {
}
extension Rule.Alternatives: Decodable {
public init(from decoder: Decoder) throws {
let boolContainer = try BoolContainer(from: decoder)
self = boolContainer.rawValue ? .true : .false
}
}
......@@ -21,15 +21,19 @@ extension Rule {
/// Indicates if the rule can be applied with alternatives.
public var alternatives: Alternatives?
public var filters: String?
public init(anchoring: Anchoring? = nil,
pattern: Pattern? = nil,
context: String? = nil,
alternatives: Alternatives? = nil) {
alternatives: Alternatives? = nil,
filters: String? = nil) {
self.anchoring = anchoring
self.pattern = pattern
self.context = context
self.alternatives = alternatives
self.filters = filters
}
}
......
//
// Answer.swift
//
//
// Created by Vladislav Fitc on 19/11/2020.
//
import Foundation
public struct Answer: Codable, Hashable {
/// String containing the answer
public let extract: HighlightedString
/// Attribute the answer is come from
public let extractAttribute: Attribute
/// Answer's confidence score
public let score: Double
}
......@@ -13,10 +13,19 @@ public struct Hit<T: Codable> {
public let objectID: ObjectID
public let object: T
/// Snippeted attributes. Only returned when `attributesToSnippet` is non-empty.
public let snippetResult: TreeModel<SnippetResult>?
/// Highlighted attributes. Only returned when `attributesToHighlight` is non-empty.
public let highlightResult: TreeModel<HighlightResult>?
/// Ranking information. Only returned when `getRankingInfo` is true.
public let rankingInfo: RankingInfo?
public let geolocation: Point?
public let geolocation: SingleOrList<Point>?
/// Answer information
public let answer: Answer?
}
......@@ -30,6 +39,7 @@ extension Hit: Codable {
case highlightResult = "_highlightResult"
case rankingInfo = "_rankingInfo"
case geolocation = "_geoloc"
case answer = "_answer"
}
public init(from decoder: Decoder) throws {
......@@ -39,12 +49,19 @@ extension Hit: Codable {
self.snippetResult = try container.decodeIfPresent(TreeModel<SnippetResult>.self, forKey: .snippetResult)
self.highlightResult = try container.decodeIfPresent(TreeModel<HighlightResult>.self, forKey: .highlightResult)
self.rankingInfo = try container.decodeIfPresent(RankingInfo.self, forKey: .rankingInfo)
self.geolocation = try container.decodeIfPresent(Point.self, forKey: .geolocation)
self.geolocation = try? container.decodeIfPresent(SingleOrList<Point>.self, forKey: .geolocation)
self.answer = try container.decodeIfPresent(forKey: .answer)
}
public func encode(to encoder: Encoder) throws {
var keyedContainer = encoder.container(keyedBy: CodingKeys.self)
try keyedContainer.encode(objectID, forKey: .objectID)
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encodeIfPresent(objectID, forKey: .objectID)
try container.encodeIfPresent(objectID, forKey: .objectID)
try container.encodeIfPresent(snippetResult, forKey: .snippetResult)
try container.encodeIfPresent(highlightResult, forKey: .highlightResult)
try container.encodeIfPresent(rankingInfo, forKey: .rankingInfo)
try container.encodeIfPresent(geolocation, forKey: .geolocation)
try container.encodeIfPresent(answer, forKey: .answer)
try object.encode(to: encoder)
}
......
......@@ -8,7 +8,7 @@
import Foundation
public struct PartialUpdate: Equatable {
typealias Storage = [Attribute: Action]
let storage: Storage
......@@ -16,11 +16,11 @@ public struct PartialUpdate: Equatable {
}
extension PartialUpdate: ExpressibleByDictionaryLiteral {
public init(dictionaryLiteral elements: (Attribute, Action)...) {
self.init(storage: .init(uniqueKeysWithValues: elements))
}
}
public extension PartialUpdate {
......@@ -35,7 +35,7 @@ public extension PartialUpdate {
}
public extension PartialUpdate {
/// Increment a numeric attribute
/// - Parameter attribute: Attribute name to update
/// - Parameter value: Value to increment by
......@@ -75,7 +75,7 @@ public extension PartialUpdate {
static func incrementFrom(attribute: Attribute, value: Int) -> Self {
.operation(attribute: attribute, operation: .incrementFrom, value: .init(value))
}
/**
Increment a numeric integer attribute only if the provided value is greater than the current value,
and otherwise ignore the whole object update.
......@@ -93,7 +93,6 @@ public extension PartialUpdate {
}
public extension PartialUpdate {
/// Decrement a numeric attribute
......@@ -102,7 +101,7 @@ public extension PartialUpdate {
static func decrement(attribute: Attribute, value: Int) -> Self {
.operation(attribute: attribute, operation: .decrement, value: .init(value))
}
/// Decrement a numeric attribute
/// - Parameter attribute: Attribute name to update
/// - Parameter value: Value to decrement by
......@@ -209,21 +208,21 @@ extension PartialUpdate: Codable {
}
extension PartialUpdate {
struct Operation: Codable, Equatable {
let kind: Kind
let value: JSON
enum CodingKeys: String, CodingKey {
case value
case kind = "_operation"
}
enum Kind: String, Codable {
/// Increment a numeric attribute
case increment = "Increment"
/**
Increment a numeric integer attribute only if the provided value matches the current value,
and otherwise ignore the whole object update.
......@@ -233,7 +232,7 @@ extension PartialUpdate {
If the object doesn’t exist, the engine only creates it if you pass an IncrementFrom value of 0.
*/
case incrementFrom = "IncrementFrom"
/**
Increment a numeric integer attribute only if the provided value is greater than the current value,
and otherwise ignore the whole object update.
......@@ -243,20 +242,20 @@ extension PartialUpdate {
If the object doesn’t exist yet, the engine only creates it if you pass an IncrementSet value that’s greater than 0.
*/
case incrementSet = "IncrementSet"
/// Decrement a numeric attribute
case decrement = "Decrement"
/// Append a number or string element to an array attribute
case add = "Add"
/// Remove all matching number or string elements from an array attribute
case remove = "Remove"
/// Add a number or string element to an array attribute only if it’s not already present
case addUnique = "AddUnique"
}
}
}
......@@ -8,19 +8,19 @@
import Foundation
extension PartialUpdate {
public struct Action: Equatable {
let storage: Storage
}
}
extension PartialUpdate.Action {
enum Storage: Codable, Equatable {
case set(JSON)
case operation(PartialUpdate.Operation)
public init(from decoder: Decoder) throws {
if let operation = try? PartialUpdate.Operation(from: decoder) {
self = .operation(operation)
......@@ -28,7 +28,7 @@ extension PartialUpdate.Action {
self = .set(try JSON(from: decoder))
}
}
public func encode(to encoder: Encoder) throws {
switch self {
case .set(let value):
......@@ -39,23 +39,23 @@ extension PartialUpdate.Action {
}
}
}
extension PartialUpdate.Action: Decodable {
public init(from decoder: Decoder) throws {
self.storage = try Storage(from: decoder)
}
}
extension PartialUpdate.Action: Encodable {
public func encode(to encoder: Encoder) throws {
try storage.encode(to: encoder)
}
}
public extension PartialUpdate.Action {
......@@ -63,7 +63,7 @@ public extension PartialUpdate.Action {
init(_ json: JSON) {
storage = .set(json)
}
static func set(_ json: JSON) -> Self {
return .init(json)
}
......@@ -127,15 +127,15 @@ extension PartialUpdate.Action: ExpressibleByNilLiteral {
}
extension PartialUpdate.Action {
init(operation: PartialUpdate.Operation.Kind, value: JSON) {
storage = .operation(.init(kind: operation, value: value))
}
}
public extension PartialUpdate.Action {
/// Increment a numeric attribute
/// - Parameter value: Value to increment by
static func increment(value: Int) -> Self {
......@@ -171,7 +171,7 @@ public extension PartialUpdate.Action {
static func incrementFrom(value: Int) -> Self {
.init(operation: .incrementFrom, value: .init(value))
}
/**
Increment a numeric integer attribute only if the provided value is greater than the current value,
and otherwise ignore the whole object update.
......@@ -188,7 +188,6 @@ public extension PartialUpdate.Action {
}
public extension PartialUpdate.Action {
/// Decrement a numeric attribute
......@@ -196,7 +195,7 @@ public extension PartialUpdate.Action {
static func decrement(value: Int) -> Self {
.init(operation: .decrement, value: .init(value))
}
/// Decrement a numeric attribute
/// - Parameter value: Value to decrement by
static func decrement(value: Float) -> Self {
......
......@@ -11,7 +11,7 @@ public struct SearchesResponse: Codable {
/// List of result in the order they were submitted, one element for each IndexQuery.
public var results: [SearchResponse]
public init(results: [SearchResponse]) {
self.results = results
}
......
......@@ -84,8 +84,11 @@ extension Point: Codable {
self = firstPoint.point
} else if let dictionaryForm = try? DictionaryForm(from: decoder) {
self = dictionaryForm.point
} else if let stringForm = try? StringForm(from: decoder) {
self = stringForm.point
} else {
self = try StringForm(from: decoder).point
let encoded = (try? String(data: JSONEncoder().encode(JSON(from: decoder)), encoding: .utf8)) ?? ""
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: [], debugDescription: "The format \(encoded) doesn't match \"22.2268,84.8463\" string or {\"lat\": 22.2268, \"lng\": 84.8463 } dictionary"))
}
}
......
......@@ -8,7 +8,7 @@
import Foundation
extension Query: Codable {
extension SearchParametersStorage: Codable {
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
......@@ -50,6 +50,7 @@ extension Query: Codable {
ignorePlurals = try container.decodeIfPresent(forKey: .ignorePlurals)
removeStopWords = try container.decodeIfPresent(forKey: .removeStopWords)
queryLanguages = try container.decodeIfPresent(forKey: .queryLanguages)
decompoundQuery = try container.decodeIfPresent(forKey: .decompoundQuery)
enableRules = try container.decodeIfPresent(forKey: .enableRules)
ruleContexts = try container.decodeIfPresent(forKey: .ruleContexts)
enablePersonalization = try container.decodeIfPresent(forKey: .enablePersonalization)
......@@ -78,7 +79,6 @@ extension Query: Codable {
enableABTest = try container.decodeIfPresent(forKey: .enableABTest)
explainModules = try container.decodeIfPresent(forKey: .explainModules)
naturalLanguages = try container.decodeIfPresent(forKey: .naturalLanguages)
customParameters = try CustomParametersCoder.decode(from: decoder, excludingKeys: CodingKeys.self)
}
public func encode(to encoder: Encoder) throws {
......@@ -121,6 +121,7 @@ extension Query: Codable {
try container.encodeIfPresent(ignorePlurals, forKey: .ignorePlurals)
try container.encodeIfPresent(removeStopWords, forKey: .removeStopWords)
try container.encodeIfPresent(queryLanguages, forKey: .queryLanguages)
try container.encodeIfPresent(decompoundQuery, forKey: .decompoundQuery)
try container.encodeIfPresent(enableRules, forKey: .enableRules)
try container.encodeIfPresent(ruleContexts, forKey: .ruleContexts)
try container.encodeIfPresent(enablePersonalization, forKey: .enablePersonalization)
......@@ -149,9 +150,6 @@ extension Query: Codable {
try container.encodeIfPresent(enableABTest, forKey: .enableABTest)
try container.encodeIfPresent(explainModules, forKey: .explainModules)
try container.encodeIfPresent(naturalLanguages, forKey: .naturalLanguages)
if let customParameters = customParameters {
try CustomParametersCoder.encode(customParameters, to: encoder)
}
}
enum CodingKeys: String, CodingKey, CaseIterable {
......@@ -193,6 +191,7 @@ extension Query: Codable {
case ignorePlurals
case removeStopWords
case queryLanguages
case decompoundQuery
case enableRules
case ruleContexts
case enablePersonalization
......
......@@ -12,7 +12,7 @@ extension Query {
struct URLEncoder<Key: RawRepresentable> where Key.RawValue == String {
var queryItems: [URLQueryItem] = []
mutating func set<T: URLEncodable>(_ value: T?, for key: Key) {
guard let value = value else { return }
queryItems.append(.init(name: key.rawValue, value: value.urlEncodedString))
......@@ -114,7 +114,7 @@ extension Query: URLEncodable {
public var urlEncodedString: String {
var urlEncoder = URLEncoder<CodingKeys>()
var urlEncoder = URLEncoder<SearchParametersStorage.CodingKeys>()
urlEncoder.set(query, for: .query)
urlEncoder.set(similarQuery, for: .similarQuery)
......@@ -167,6 +167,7 @@ extension Query: URLEncodable {
urlEncoder.set(alternativesAsExact, for: .alternativesAsExact)
urlEncoder.set(ignorePlurals, for: .ignorePlurals)
urlEncoder.set(queryLanguages, for: .queryLanguages)
urlEncoder.set(decompoundQuery, for: .decompoundQuery)
urlEncoder.set(enableRules, for: .enableRules)
urlEncoder.set(ruleContexts, for: .ruleContexts)
urlEncoder.set(enablePersonalization, for: .enablePersonalization)
......
......@@ -12,24 +12,24 @@ public struct TaggedString {
/// Input string
public let input: String
/// Opening tag
public let preTag: String
/// Closing tag
public let postTag: String
/// String comparison options for tags parsing
public let options: String.CompareOptions
private lazy var storage: (String, [Range<String.Index>]) = TaggedString.computeRanges(for: input, preTag: preTag, postTag: postTag, options: options)
/// Output string cleansed of the opening and closing tags
public private(set) lazy var output: String = { storage.0 }()
/// List of ranges of tagged substrings in the output string
public private(set) lazy var taggedRanges: [Range<String.Index>] = { storage.1 }()
/// List of ranges of untagged substrings in the output string
public private(set) lazy var untaggedRanges: [Range<String.Index>] = TaggedString.computeInvertedRanges(for: output, with: taggedRanges)
......@@ -52,12 +52,12 @@ public struct TaggedString {
self.postTag = postTag
self.options = options
}
/// - Returns: The list of tagged substrings of the output string
public mutating func taggedSubstrings() -> [Substring] {
return taggedRanges.map { output[$0] }
}
/// - Returns: The list of untagged substrings of the output string
public mutating func untaggedSubstrings() -> [Substring] {
return untaggedRanges.map { output[$0] }
......@@ -65,15 +65,13 @@ public struct TaggedString {
private static func computeRanges(for string: String, preTag: String, postTag: String, options: String.CompareOptions) -> (output: String, ranges: [Range<String.Index>]) {
var output = string
var preStack: [R] = []
var rangesToHighlight = [R]()
typealias R = Range<String.Index>
var preStack: [Range<String.Index>] = []
var rangesToHighlight = [Range<String.Index>]()
enum Tag {
case pre(R), post(R)
case pre(Range<String.Index>), post(Range<String.Index>)
}
func nextTag(in string: String) -> Tag? {
switch (string.range(of: preTag, options: options), string.range(of: postTag, options: options)) {
case (.some(let pre), .some(let post)) where pre.lowerBound < post.lowerBound:
......@@ -88,7 +86,7 @@ public struct TaggedString {
return nil
}
}
while let nextTag = nextTag(in: output) {
switch nextTag {
case .pre(let preRange):
......@@ -102,18 +100,18 @@ public struct TaggedString {
output.removeSubrange(postRange)
}
}
return (output, mergeOverlapping(rangesToHighlight))
}
private static func computeInvertedRanges(for string: String, with ranges: [Range<String.Index>]) -> [Range<String.Index>] {
if ranges.isEmpty {
return ([string.startIndex..<string.endIndex])
}
var lowerBound = string.startIndex
var invertedRanges: [Range<String.Index>] = []
for range in ranges where range.lowerBound >= lowerBound {
......@@ -131,7 +129,7 @@ public struct TaggedString {
return invertedRanges
}
private static func mergeOverlapping<T: Comparable>(_ input: [Range<T>]) -> [Range<T>] {
var output: [Range<T>] = []
let sortedRanges = input.sorted(by: { $0.lowerBound < $1.lowerBound })
......@@ -157,11 +155,11 @@ public struct TaggedString {
}
extension TaggedString: Hashable {
public func hash(into hasher: inout Hasher) {
input.hash(into: &hasher)
preTag.hash(into: &hasher)
postTag.hash(into: &hasher)
}
}
......@@ -224,11 +224,11 @@ public struct SearchResponse {
Meta-information as to how the query was processed.
*/
public var explain: Explain?
public init(hits: [Hit<JSON>] = []) {
self.hits = hits
}
}
extension SearchResponse: Builder {}
......
//
// SearchParameters.swift
//
//
// Created by Vladislav Fitc on 19/11/2020.
//
import Foundation
public protocol SearchParameters: CommonParameters {
// MARK: - Search
/**
The text to search in the index.
- Engine default: ""
- [Documentation](https://www.algolia.com/doc/api-reference/api-parameters/query/?language=swift)
*/
var query: String? { get set }
/**
Overrides the query parameter and performs a more generic search that can be used to find "similar" results.
Engine default: ""
[Documentation][htt ps://www.algolia.com/doc/api-reference/api-parameters/similarQuery/?language=swift)
*/
var similarQuery: String? { get set }
// MARK: - Attributes
/**
Restricts a given query to look in only a subset of your searchable attributes.
- Engine default: all attributes in [Settings.searchableAttributes].
- [Documentation](https://www.algolia.com/doc/api-reference/api-parameters/restrictSearchableAttributes/?language=swift)
*/
var restrictSearchableAttributes: [Attribute]? { get set }
// MARK: - Filtering
/**
Filter the query with numeric, facet and/or tag filters.
- Engine default: ""
- [Documentation](https://www.algolia.com/doc/api-reference/api-parameters/filters/?language=swift)
*/
var filters: String? { get set }
/**
Filter hits by facet value.
- Engine default: []
- [Documentation](https://www.algolia.com/doc/api-reference/api-parameters/facetFilters/?language=swift)
*/
var facetFilters: FiltersStorage? { get set }
/**
Create filters for ranking purposes, where records that match the filter are ranked highest.
- Engine default: []
- [Documentation](https://www.algolia.com/doc/api-reference/api-parameters/optionalFilters/?language=swift)
*/
var optionalFilters: FiltersStorage? { get set }
/**
Filter on numeric attributes.
- Engine default: []
- [Documentation](https://www.algolia.com/doc/api-reference/api-parameters/numericFilters/?language=swift)
*/
var numericFilters: FiltersStorage? { get set }
/**
Filter hits by tags.
- Engine default: []
- [Documentation](https://www.algolia.com/doc/api-reference/api-parameters/tagFilters/?language=swift)
*/
var tagFilters: FiltersStorage? { get set }
/**
Determines how to calculate the total score for filtering.
- Engine default: false
- [Documentation](https://www.algolia.com/doc/api-reference/api-parameters/sumOrFiltersScores/?language=swift)
*/
var sumOrFiltersScores: Bool? { get set }
// MARK: - Faceting
/**
Facets to retrieve.
- Engine default: []
- [Documentation](https://www.algolia.com/doc/api-reference/api-parameters/facets/?language=swift)
*/
var facets: Set<Attribute>? { get set }
/**
Force faceting to be applied after de-duplication (via the Distinct setting).
- Engine default: false
- [Documentation](https://www.algolia.com/doc/api-reference/api-parameters/facetingAfterDistinct/?language=swift)
*/
var facetingAfterDistinct: Bool? { get set }
// MARK: - Pagination
/**
Specify the page to retrieve.
- Engine default: 0
- [Documentation](https://www.algolia.com/doc/api-reference/api-parameters/page/?language=swift)
*/
var page: Int? { get set }
/**
Set the number of hits per page.
- Engine default: 20
- [Documentation](https://www.algolia.com/doc/api-reference/api-parameters/hitsPerPage/?language=swift)
*/
var hitsPerPage: Int? { get set }
/**
Specify the offset of the first hit to return.
- Engine default: null
- [Documentation](https://www.algolia.com/doc/api-reference/api-parameters/offset/?language=swift)
*/
var offset: Int? { get set }
/**
Set the number of hits to retrieve (used only with offset).
- Engine default: null
- [Documentation](https://www.algolia.com/doc/api-reference/api-parameters/length/?language=swift)
*/
var length: Int? { get set }
// MARK: - Geo-Search
/**
Search for entries around a central geolocation, enabling a geo search within a circular area.
- Engine default: null
- [Documentation](https://www.algolia.com/doc/api-reference/api-parameters/aroundLatLng/?language=swift)
*/
var aroundLatLng: Point? { get set }
/**
Whether to search entries around a given location automatically computed from the requester’s IP address.
- Engine default: false
- [Documentation](https://www.algolia.com/doc/api-reference/api-parameters/aroundLatLngViaIP/?language=swift)
*/
var aroundLatLngViaIP: Bool? { get set }
/**
Define the maximum radius for a geo search (in meters).
- Engine default: null
- [Documentation](https://www.algolia.com/doc/api-reference/api-parameters/aroundRadius/?language=swift)
*/
var aroundRadius: AroundRadius? { get set }
/**
Precision of geo search (in meters), to add grouping by geo location to the ranking formula.
- Engine default: 1
- [Documentation](https://www.algolia.com/doc/api-reference/api-parameters/aroundPrecision/?language=swift)
*/
var aroundPrecision: [AroundPrecision]? { get set }
/**
Minimum radius (in meters) used for a geo search when [aroundRadius] is not set.
- Engine default: null
- [Documentation](https://www.algolia.com/doc/api-reference/api-parameters/minimumAroundRadius/?language=swift)
*/
var minimumAroundRadius: Int? { get set }
/**
Search inside a rectangular area (in geo coordinates).
- Engine default: null
- [Documentation](https://www.algolia.com/doc/api-reference/api-parameters/insideBoundingBox/?language=swift)
*/
var insideBoundingBox: [BoundingBox]? { get set }
/**
Search inside a polygon (in geo coordinates).
- Engine default: null
- [Documentation](https://www.algolia.com/doc/api-reference/api-parameters/insidePolygon/?language=swift)
*/
var insidePolygon: [Polygon]? { get set }
// MARK: - Languages
/**
List of supported languages with their associated language ISO code.
Provide an easy way to implement voice and natural languages best practices such as ignorePlurals,
removeStopWords, removeWordsIfNoResults, analyticsTags and ruleContexts.
*/
var naturalLanguages: [Language]? { get set }
// MARK: - Query rules
/**
Enables contextual rules.
- Engine default: []
- [Documentation](https://www.algolia.com/doc/api-reference/api-parameters/ruleContexts/?language=swift)
*/
var ruleContexts: [String]? { get set }
// MARK: - Personalization
/**
The `personalizationImpact` parameter sets the percentage of the impact that personalization has on ranking
records.
This is set at query time and therefore overrides any impact value you had set on your index.
The higher the `personalizationImpact`, the more the results are personalized for the user, and the less the
custom ranking is taken into account in ranking records.
*
Usage note:
*
- The value must be between 0 and 100 (inclusive).
- This parameter isn't taken into account if `enablePersonalization` is `false`.
- Setting `personalizationImpact` to `0` disables the Personalization feature, as if `enablePersonalization`
were `false`.
*/
var personalizationImpact: Int? { get set }
/**
Associates a certain user token with the current search.
- Engine default: User ip address
- [Documentation](https://www.algolia.com/doc/api-reference/api-parameters/userToken/?language=swift)
*/
var userToken: UserToken? { get set }
// MARK: - Advanced
/**
Retrieve detailed ranking information.
- Engine default: false
- [Documentation](https://www.algolia.com/doc/api-reference/api-parameters/getRankingInfo/?language=swift)
*/
var getRankingInfo: Bool? { get set }
/**
Enable the Click Analytics feature.
- Engine default: false.
- [Documentation](https://www.algolia.com/doc/api-reference/api-parameters/clickAnalytics/?language=swift)
*/
var clickAnalytics: Bool? { get set }
/**
Whether the current query will be taken into account in the Analytics.
- Engine default: true
- [Documentation](https://www.algolia.com/doc/api-reference/api-parameters/analytics/?language=swift)
*/
var analytics: Bool? { get set }
/**
List of tags to apply to the query in the analytics.
- Engine default: []
- [Documentation](https://www.algolia.com/doc/api-reference/api-parameters/analyticsTags/?language=swift)
*/
var analyticsTags: [String]? { get set }
/**
Whether to take into account an index’s synonyms for a particular search.
- Engine default: true
- [Documentation](https://www.algolia.com/doc/api-reference/api-parameters/synonyms/?language=swift)
*/
var synonyms: Bool? { get set }
/**
Whether to include or exclude a query from the processing-time percentile computation.
- Engine default: true
- [Documentation](https://www.algolia.com/doc/api-reference/api-parameters/percentileComputation/?language=swift)
*/
var percentileComputation: Bool? { get set }
/**
Whether this query should be taken into consideration by currently active ABTests.
- Engine default: true
- [Documentation](https://www.algolia.com/doc/api-reference/api-parameters/enableABTest/?language=swift)
*/
var enableABTest: Bool? { get set }
/**
Enriches the API’s response with meta-information as to how the query was processed.
It is possible to enable several ExplainModule independently.
- Engine default: null
- [Documentation](https://www.algolia.com/doc/api-reference/api-parameters/_/?language=swift)
*/
var explainModules: [ExplainModule]? { get set }
}
......@@ -56,7 +56,7 @@ extension LanguageFeature: Encodable {
}
extension LanguageFeature: Decodable {
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let boolContainer = try? container.decode(BoolContainer.self) {
......@@ -66,7 +66,7 @@ extension LanguageFeature: Decodable {
self = .queryLanguages(languages)
}
}
}
extension LanguageFeature: Equatable {}
......
//
// SettingsParameters.swift
//
//
// Created by Vladislav Fitc on 19/11/2020.
//
import Foundation
public protocol SettingsParameters: CommonParameters {
// MARK: - Attributes
/**
The complete list of attributes that will be used for searching.
- Engine default: []
- [Documentation](https://www.algolia.com/doc/api-reference/api-parameters/searchableAttributes/?language=swift)
*/
var searchableAttributes: [SearchableAttribute]? { get set }
/**
The complete list of attributes that will be used for faceting.
- Engine default: []
- [Documentation](https://www.algolia.com/doc/api-reference/api-parameters/attributesForFaceting/?language=swift)
*/
var attributesForFaceting: [AttributeForFaceting]? { get set }
/**
List of attributes that cannot be retrieved at query time.
- Engine default: []
- [Documentation](https://www.algolia.com/doc/api-reference/api-parameters/unretrievableAttributes/?language=swift)
*/
var unretrievableAttributes: [Attribute]? { get set }
// MARK: - Ranking
/**
Controls the way results are sorted.
- Engine default: [.typo, .geo, .words, .filters, .proximity, .attribute, .exact, .custom]
- [Documentation](https://www.algolia.com/doc/api-reference/api-parameters/ranking/?language=swift)
*/
var ranking: [RankingCriterion]? { get set }
/**
Specifies the [CustomRankingCriterion].
- Engine default: []
- [Documentation](https://www.algolia.com/doc/api-reference/api-parameters/customRanking/?language=swift)
*/
var customRanking: [CustomRankingCriterion]? { get set }
/**
Creates replicas, exact copies of an index.
- Engine default: []
- [Documentation](https://www.algolia.com/doc/api-reference/api-parameters/replicas/?language=swift)
*/
var replicas: [IndexName]? { get set }
// MARK: - Pagination
/**
Set the maximum number of hits accessible via pagination.
- Engine default: 1000
- [Documentation](https://www.algolia.com/doc/api-reference/api-parameters/paginationlimitedto/?language=swift)
*/
var paginationLimitedTo: Int? { get set }
// MARK: - Typos
/**
List of words on which you want to disable typo tolerance.
- Engine default: []
- [Documentation](https://www.algolia.com/doc/api-reference/api-parameters/disableTypoToleranceOnWords/?language=swift)
*/
var disableTypoToleranceOnWords: [String]? { get set }
/**
Control which separators are indexed. Separators are all non-alphanumeric characters except space.
- [Documentation](https://www.algolia.com/doc/api-reference/api-parameters/separatorsToIndex/?language=swift)
*/
var separatorsToIndex: String? { get set }
// MARK: - Languages
/**
Specify on which attributes to apply transliteration.
Transliteration refers to the ability of finding results in a given alphabet with a query in another alphabet. For example, in Japanese, transliteration enables users to find results indexed in Kanji or Katakana with a query in Hiragana.
- Engine default: [*]
- [Documentation](https://www.algolia.com/doc/api-reference/api-parameters/attributesToTransliterate/?language=swift)
*/
var attributesToTransliterate: [Attribute]? { get set }
/**
List of [Attribute] on which to do a decomposition of camel case words.
- Engine default: []
- [Documentation](https://www.algolia.com/doc/api-reference/api-parameters/camelCaseAttributes/?language=swift)
*/
var camelCaseAttributes: [Attribute]? { get set }
/**
Specify on which [Attribute] in your index Algolia should apply word-splitting (“decompounding”).
- Engine default: []
- [Documentation](https://www.algolia.com/doc/api-reference/api-parameters/decompoundedAttributes/?language=swift)
*/
var decompoundedAttributes: DecompoundedAttributes? { get set }
/**
Characters that should not be automatically normalized by the search engine.
- Engine default: "&quot;&quot;"
- [Documentation](https://www.algolia.com/doc/api-reference/api-parameters/keepDiacriticsOnCharacters/?language=swift)
*/
var keepDiacriticsOnCharacters: String? { get set }
/**
Override the custom normalization handled by the engine.
*/
var customNormalization: [String: [String: String]]? { get set }
/**
This parameter configures the segmentation of text at indexing time.
- Accepted value: Language.japanese
- Input data to index is treated as the given language(s) for segmentation.
*/
var indexLanguages: [Language]? { get set }
// MARK: - Query strategy
/**
List of [Attribute] on which you want to disable prefix matching.
- Engine default: []
- [Documentation](https://www.algolia.com/doc/api-reference/api-parameters/disablePrefixOnAttributes/?language=swift)
*/
var disablePrefixOnAttributes: [Attribute]? { get set }
// MARK: - Performance
/**
List of [NumericAttributeFilter] that can be used as numerical filters.
- Engine default: null
- [Documentation](https://www.algolia.com/doc/api-reference/api-parameters/numericAttributesForFiltering/?language=swift)
*/
var numericAttributesForFiltering: [NumericAttributeFilter]? { get set }
/**
Enables compression of large integer arrays.
- Engine default: false
- [Documentation](https://www.algolia.com/doc/api-reference/api-parameters/allowCompressionOfIntegerArray/?language=swift)
*/
var allowCompressionOfIntegerArray: Bool? { get set }
// MARK: - Advanced
/**
Name of the de-duplication [Attribute] to be used with the [distinct] feature.
- Engine default: null
- [Documentation](https://www.algolia.com/doc/api-reference/api-parameters/attributeForDistinct/?language=swift)
*/
var attributeForDistinct: Attribute? { get set }
/**
Lets you store custom data in your indices.
*/
var userData: JSON? { get set }
/**
Settings version.
*/
var version: Int? { get set }
/**
This parameter keeps track of which primary index (if any) a replica is connected to.
*/
var primary: IndexName? { get set }
}
//
// Settings+Codable.swift
// AlgoliaSearchClient
// SettingsParametersCodingKeys.swift
//
//
// Created by Vladislav Fitc on 24/03/2020.
// Created by Vladislav Fitc on 20/11/2020.
//
import Foundation
extension Settings {
public typealias Key = CodingKeys
public enum CodingKeys: String, CodingKey {
case searchableAttributes
case attributesForFaceting
case unretrievableAttributes
case attributesToRetrieve
case ranking
case customRanking
case replicas
case maxValuesPerFacet
case sortFacetsBy = "sortFacetValuesBy"
case attributesToHighlight
case attributesToSnippet
case highlightPreTag
case highlightPostTag
case snippetEllipsisText
case restrictHighlightAndSnippetArrays
case hitsPerPage
case paginationLimitedTo
case minWordSizeFor1Typo = "minWordSizefor1Typo"
case minWordSizeFor2Typos = "minWordSizefor2Typos"
case typoTolerance
case allowTyposOnNumericTokens
case disableTypoToleranceOnAttributes
case disableTypoToleranceOnWords
case separatorsToIndex
case ignorePlurals
case removeStopWords
case camelCaseAttributes
case decompoundedAttributes
case keepDiacriticsOnCharacters
case queryLanguages
case enableRules
case enablePersonalization
case queryType
case removeWordsIfNoResults
case advancedSyntax
case advancedSyntaxFeatures
case optionalWords
case disablePrefixOnAttributes
case disableExactOnAttributes
case exactOnSingleWordQuery
case alternativesAsExact
case numericAttributesForFiltering
case allowCompressionOfIntegerArray
case attributeForDistinct
case distinct
case replaceSynonymsInHighlight
case minProximity
case responseFields
case maxFacetHits
case userData
case indexLanguages
case customNormalization
case primary
case attributeCriteriaComputedByMinProximity
}
public enum SettingsParametersCodingKeys: String, CodingKey {
case searchableAttributes
case attributesForFaceting
case unretrievableAttributes
case attributesToRetrieve
case ranking
case customRanking
case replicas
case maxValuesPerFacet
case sortFacetsBy = "sortFacetValuesBy"
case attributesToHighlight
case attributesToSnippet
case highlightPreTag
case highlightPostTag
case snippetEllipsisText
case restrictHighlightAndSnippetArrays
case hitsPerPage
case paginationLimitedTo
case minWordSizeFor1Typo = "minWordSizefor1Typo"
case minWordSizeFor2Typos = "minWordSizefor2Typos"
case typoTolerance
case allowTyposOnNumericTokens
case disableTypoToleranceOnAttributes
case disableTypoToleranceOnWords
case separatorsToIndex
case ignorePlurals
case removeStopWords
case attributesToTransliterate
case camelCaseAttributes
case decompoundedAttributes
case keepDiacriticsOnCharacters
case queryLanguages
case decompoundQuery
case enableRules
case enablePersonalization
case queryType
case removeWordsIfNoResults
case advancedSyntax
case advancedSyntaxFeatures
case optionalWords
case disablePrefixOnAttributes
case disableExactOnAttributes
case exactOnSingleWordQuery
case alternativesAsExact
case numericAttributesForFiltering
case allowCompressionOfIntegerArray
case attributeForDistinct
case distinct
case replaceSynonymsInHighlight
case minProximity
case responseFields
case maxFacetHits
case userData
case indexLanguages
case customNormalization
case primary
case attributeCriteriaComputedByMinProximity
}
//
// AppTaskID.swift
//
//
// Created by Vladislav Fitc on 01/02/2021.
//
import Foundation
// Identifier of an app-level task
public struct AppTaskID: StringWrapper {
public let rawValue: String
public init(rawValue: String) {
self.rawValue = rawValue
}
}
//
// AppTask.swift
//
//
// Created by Vladislav Fitc on 01/02/2021.
//
import Foundation
public protocol AppTask {
var taskID: AppTaskID { get }
}
......@@ -13,7 +13,7 @@ extension HTTPTransport {
case noReachableHosts
case missingData
case decodingFailure(Swift.Error)
var localizedDescription: String {
switch self {
case .noReachableHosts:
......
......@@ -25,19 +25,22 @@ class AlgoliaRetryStrategy: RetryStrategy {
convenience init(configuration: Configuration) {
self.init(hosts: configuration.hosts)
}
func retryableHosts(for callType: CallType) -> HostIterator {
return queue.sync {
// Reset expired hosts
hosts.resetExpired(expirationDelay: hostsExpirationDelay)
// If all hosts of the required type are down, reset them all
if !hosts.contains(where: { $0.supports(callType) && $0.isUp }) {
hosts.resetAll(for: callType)
}
return HostIterator { [weak self] in
self?.hosts.first { $0.supports(callType) && $0.isUp }
guard let retryStrategy = self else { return nil }
return retryStrategy.queue.sync {
return retryStrategy.hosts.first { $0.supports(callType) && $0.isUp }
}
}
}
}
......
......@@ -10,7 +10,7 @@ import Foundation
class HostIterator: IteratorProtocol {
var getHost: () -> RetryableHost?
init(getHost: @escaping () -> RetryableHost?) {
self.getHost = getHost
}
......
......@@ -13,9 +13,9 @@ import FoundationNetworking
extension URLRequest: Builder {}
extension CharacterSet {
static var uriAllowed = CharacterSet.urlQueryAllowed.subtracting(.init(charactersIn: "+"))
}
extension URLRequest {
......@@ -37,7 +37,7 @@ extension URLRequest {
var urlComponents = URLComponents()
urlComponents.scheme = "https"
urlComponents.path = path.fullPath
if let urlParameters = requestOptions?.urlParameters {
urlComponents.queryItems = urlParameters.map { (key, value) in .init(name: key.rawValue, value: value) }
}
......@@ -46,22 +46,22 @@ extension URLRequest {
request.httpMethod = method.rawValue
request.httpBody = body
if let requestOptions = requestOptions {
requestOptions.headers.forEach { header in
let (value, field) = (header.value, header.key.rawValue)
request.setValue(value, forHTTPHeaderField: field)
}
// If body is set in query parameters, it will override the body passed as parameter to this function
if let body = requestOptions.body, !body.isEmpty {
let jsonEncodedBody = try? JSONSerialization.data(withJSONObject: body, options: [])
request.httpBody = jsonEncodedBody
}
}
request.httpBody = body
self = request
}
......
......@@ -17,8 +17,7 @@ extension URLSession: HTTPRequester {
public func perform<T: Decodable>(request: URLRequest, completion: @escaping (Result<T, Error>) -> Void) -> TransportTask {
let task = dataTask(with: request) { (data, response, error) in
let result = Result<T, Error>(data: data, response: response, error: error)
if case .success = result,
let data = data,
if let data = data,
let responseJSON = try? JSONDecoder().decode(JSON.self, from: data) {
Logger.loggingService.log(level: .debug, message: "Response: \(responseJSON)")
}
......
PODS:
- AlgoliaSearchClient (8.3.0):
- AlgoliaSearchClient (8.6.0):
- Logging
- Logging (1.4.0)
- SnapKit (5.0.1)
- synth-ios (1.0.0)
DEPENDENCIES:
- AlgoliaSearchClient (~> 8.2)
- AlgoliaSearchClient
- SnapKit
- synth-ios
SPEC REPOS:
trunk:
- AlgoliaSearchClient
- Logging
- SnapKit
- synth-ios
SPEC CHECKSUMS:
AlgoliaSearchClient: 3a2f00249fc9013cd4fc0a2d27f8ac56f5219db6
AlgoliaSearchClient: 36cb507cbf100def2d028f479eb8f194cb37c604
Logging: beeb016c9c80cf77042d62e83495816847ef108b
SnapKit: 97b92857e3df3a0c71833cce143274bf6ef8e5eb
synth-ios: b91d9176ecc7ff791d94df9aedafbf5a8c00f965
PODFILE CHECKSUM: 04d64ceaca7895595534185b5c15c69d95ca82cf
PODFILE CHECKSUM: e1d8b684b1d845d76a34a4f0b3756d265fdecbf9
COCOAPODS: 1.10.0
COCOAPODS: 1.10.1
This source diff could not be displayed because it is too large. You can view the blob instead.
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1220"
LastUpgradeVersion = "1240"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment