diff --git a/.code-samples.meilisearch.yaml b/.code-samples.meilisearch.yaml index cb697362..c607f114 100644 --- a/.code-samples.meilisearch.yaml +++ b/.code-samples.meilisearch.yaml @@ -133,7 +133,7 @@ async_guide_filter_by_ids_1: |- } } async_guide_filter_by_statuses_1: |- - client.getTasks(params: TasksQuery(statuses: ["failed", "canceled"])) { result in + client.getTasks(params: TasksQuery(statuses: [.failed, .canceled])) { result in switch result { case .success(let taskResult): print(taskResult) @@ -142,7 +142,7 @@ async_guide_filter_by_statuses_1: |- } } async_guide_filter_by_types_1: |- - client.getTasks(params: TasksQuery(types: ["dumpCreation", "indexSwap"])) { result in + client.getTasks(params: TasksQuery(types: [.dumpCreation, .indexSwap])) { result in switch result { case .success(let taskResult): print(taskResult) @@ -234,6 +234,16 @@ search_parameter_guide_attributes_to_search_on_1: |- print(error) } } +search_parameter_guide_show_ranking_score_1: |- + let searchParameters = SearchParameters(query: "dragon", showRankingScore: true) + client.index("movies").search(searchParameters) { (result: Result, Swift.Error>) in + switch result { + case .success(let searchResult): + print(searchResult.rankingScore) + case .failure(let error): + print(error) + } + } authorization_header_1: |- client = try MeiliSearch(host: "http://localhost:7700", apiKey: "masterKey") client.getKeys { result in @@ -407,15 +417,6 @@ search_post_1: |- print(error) } } -get_task_by_index_1: |- - client.index("movies").getTask(taskUid: 1) { (result) in - switch result { - case .success(let task): - print(task) - case .failure(let error): - print(error) - } - } get_task_1: |- client.getTask(taskUid: 1) { (result) in switch result { @@ -426,7 +427,7 @@ get_task_1: |- } } get_all_tasks_1: |- - client.getTasks() { (result) in + client.getTasks { (result) in switch result { case .success(let tasks): print(tasks) @@ -434,6 +435,24 @@ get_all_tasks_1: |- print(error) } } +delete_tasks_1: |- + client.deleteTasks(filter: DeleteTasksQuery(uids: [1, 2])) { (result) in + switch result { + case .success(let taskInfo): + print(taskInfo) + case .failure(let error): + print(error) + } + } +cancel_tasks_1: |- + client.cancelTasks(filter: CancelTasksQuery(uids: [1, 2])) { (result) in + switch result { + case .success(let taskInfo): + print(taskInfo) + case .failure(let error): + print(error) + } + } get_one_key_1: |- client.getKey(keyOrUid: "6062abda-a5aa-4414-ac91-ecd7944c0f8d") { result in switch result { diff --git a/README.md b/README.md index 2c518cd2..c9b48ecc 100644 --- a/README.md +++ b/README.md @@ -135,7 +135,7 @@ To do a simple search using the client, you can create a Swift script like this: ) { result in switch result { case .success(let task): - print(task) // => Task(uid: 0, status: "enqueued", ...) + print(task) // => Task(uid: 0, status: Task.Status.enqueued, ...) case .failure(let error): print(error.localizedDescription) } @@ -149,24 +149,13 @@ With the `uid` of the task, you can check the status (`enqueued`, `canceled`, `p #### Basic Search ```swift - -let semaphore = DispatchSemaphore(value: 0) - -// Typealias that represents the result from Meilisearch. -typealias MeiliResult = Result, Swift.Error> - -// Call the function search and wait for the closure result. -client.index("movies").search(SearchParameters( query: "philoudelphia" )) { (result: MeiliResult) in - switch result { - case .success(let searchResult): - dump(searchResult) - case .failure(let error): - print(error.localizedDescription) - } - semaphore.signal() +do { + // Call the search function and wait for the result. + let result: SearchResult = try await client.index("movies").search(SearchParameters(query: "philoudelphia")) + dump(result) +} catch { + print(error.localizedDescription) } -semaphore.wait() - ``` Output: @@ -191,6 +180,8 @@ Output: Since Meilisearch is typo-tolerant, the movie `philadelphia` is a valid search response to `philoudelphia`. +> Note: All package APIs support closure-based results for backwards compatibility. Newer async/await variants are being added under [issue 332](https://github.com/meilisearch/meilisearch-swift/issues/332). + ## 🤖 Compatibility with Meilisearch This package guarantees compatibility with [version v1.x of Meilisearch](https://github.com/meilisearch/meilisearch/releases/latest), but some features may not be present. Please check the [issues](https://github.com/meilisearch/meilisearch-swift/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22+label%3Aenhancement) for more info. diff --git a/Sources/MeiliSearch/Async/Indexes+async.swift b/Sources/MeiliSearch/Async/Indexes+async.swift new file mode 100644 index 00000000..69380154 --- /dev/null +++ b/Sources/MeiliSearch/Async/Indexes+async.swift @@ -0,0 +1,19 @@ +import Foundation + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +extension Indexes { + /** + Search in the index. + + - Parameter searchParameters: Options on search. + - Throws: Error if a failure occurred. + - Returns: On completion if the request was successful a `Searchable` instance is returned containing the values. + */ + public func search(_ searchParameters: SearchParameters) async throws -> Searchable { + try await withCheckedThrowingContinuation { continuation in + self.search.search(self.uid, searchParameters) { result in + continuation.resume(with: result) + } + } + } +} diff --git a/Sources/MeiliSearch/Client.swift b/Sources/MeiliSearch/Client.swift index 7e2f32ba..b8567c08 100755 --- a/Sources/MeiliSearch/Client.swift +++ b/Sources/MeiliSearch/Client.swift @@ -201,6 +201,34 @@ public struct MeiliSearch { self.tasks.getTasks(params: params, completion) } + /** + Cancel any number of enqueued or processing tasks, stopping them from continuing to run + + - parameter filter: The filter in which chooses which tasks will be canceled + - parameter completion: The completion closure is used to notify when the server + completes the query request, it returns a `Result` object that contains `TaskInfo` + value. If the request was successful or `Error` if a failure occurred. + */ + public func cancelTasks( + filter: CancelTasksQuery, + completion: @escaping (Result) -> Void) { + self.tasks.cancelTasks(filter, completion) + } + + /** + Delete a finished (succeeded, failed, or canceled) task + + - parameter filter: The filter in which chooses which tasks will be deleted + - parameter completion: The completion closure is used to notify when the server + completes the query request, it returns a `Result` object that contains `TaskInfo` + value. If the request was successful or `Error` if a failure occurred. + */ + public func deleteTasks( + filter: DeleteTasksQuery, + completion: @escaping (Result) -> Void) { + self.tasks.deleteTasks(filter, completion) + } + // MARK: Keys /** diff --git a/Sources/MeiliSearch/Error.swift b/Sources/MeiliSearch/Error.swift index 774d12fa..f48d7b99 100644 --- a/Sources/MeiliSearch/Error.swift +++ b/Sources/MeiliSearch/Error.swift @@ -9,9 +9,13 @@ import Foundation public extension MeiliSearch { // MARK: Error struct MSErrorResponse: Decodable, Encodable, Equatable { + /// A human-readable description of the error public let message: String + /// The error code (https://www.meilisearch.com/docs/reference/errors/error_codes) public let code: String + /// The error type (https://www.meilisearch.com/docs/reference/errors/overview#errors) public let type: String + /// A link to the relevant section of the documentation public let link: String? } diff --git a/Sources/MeiliSearch/Indexes.swift b/Sources/MeiliSearch/Indexes.swift index 97e625dc..0f17f895 100755 --- a/Sources/MeiliSearch/Indexes.swift +++ b/Sources/MeiliSearch/Indexes.swift @@ -26,7 +26,7 @@ public struct Indexes { private let documents: Documents // Search methods - private let search: Search + internal let search: Search // Settings methods private let settings: Settings @@ -1028,6 +1028,46 @@ public struct Indexes { self.settings.resetPaginationSettings(self.uid, completion) } + // MARK: Typo Tolerance + + /** + Get the typo tolerance settings. + + - parameter completion: The completion closure is used to notify when the server + completes the query request, it returns a `Result` object that contains a `TypoToleranceResult` + value if the request was successful, or `Error` if a failure occurred. + */ + public func getTypoTolerance( + _ completion: @escaping (Result) -> Void) { + self.settings.getTypoTolerance(self.uid, completion) + } + + /** + Update the typo tolerance settings. + + - parameter typoTolerance: An object containing the settings for the `Index`. + - parameter completion: The completion closure is used to notify when the server + completes the query request, it returns a `Result` object that contains `TaskInfo` + value if the request was successful, or `Error` if a failure occurred. + */ + public func updateTypoTolerance( + _ typoTolerance: TypoTolerance, + _ completion: @escaping (Result) -> Void) { + self.settings.updateTypoTolerance(self.uid, typoTolerance, completion) + } + + /** + Reset the typo tolerance settings. + + - parameter completion: The completion closure is used to notify when the server + completes the query request, it returns a `Result` object that contains `TaskInfo` + value if the request was successful, or `Error` if a failure occurred. + */ + public func resetTypoTolerance( + _ completion: @escaping (Result) -> Void) { + self.settings.resetTypoTolerance(self.uid, completion) + } + // MARK: Stats /** diff --git a/Sources/MeiliSearch/Keys.swift b/Sources/MeiliSearch/Keys.swift index aa32adec..8f0ad8da 100644 --- a/Sources/MeiliSearch/Keys.swift +++ b/Sources/MeiliSearch/Keys.swift @@ -23,7 +23,7 @@ struct Keys { return } do { - let decoder = JSONDecoder() + let decoder = Constants.customJSONDecoder let key: Key = try decoder.decode(Key.self, from: data) completion(.success(key)) } catch { diff --git a/Sources/MeiliSearch/Model/SearchParameters.swift b/Sources/MeiliSearch/Model/SearchParameters.swift index 975ff11c..2c569ddd 100644 --- a/Sources/MeiliSearch/Model/SearchParameters.swift +++ b/Sources/MeiliSearch/Model/SearchParameters.swift @@ -62,6 +62,9 @@ public struct SearchParameters: Codable, Equatable { /// Whether to return the raw matches or not. public let showMatchesPosition: Bool? + /// Whether to return the search ranking score or not. + public let showRankingScore: Bool? + // MARK: Initializers public init( @@ -81,7 +84,8 @@ public struct SearchParameters: Codable, Equatable { filter: String? = nil, sort: [String]? = nil, facets: [String]? = nil, - showMatchesPosition: Bool? = nil) { + showMatchesPosition: Bool? = nil, + showRankingScore: Bool? = nil) { self.query = query self.offset = offset self.limit = limit @@ -99,6 +103,7 @@ public struct SearchParameters: Codable, Equatable { self.sort = sort self.facets = facets self.showMatchesPosition = showMatchesPosition + self.showRankingScore = showRankingScore } // MARK: Query Initializers @@ -133,5 +138,6 @@ public struct SearchParameters: Codable, Equatable { case showMatchesPosition case hitsPerPage case page + case showRankingScore } } diff --git a/Sources/MeiliSearch/Model/SearchResult.swift b/Sources/MeiliSearch/Model/SearchResult.swift index be576120..f1b485f3 100644 --- a/Sources/MeiliSearch/Model/SearchResult.swift +++ b/Sources/MeiliSearch/Model/SearchResult.swift @@ -11,7 +11,7 @@ public class Searchable: Equatable, Codable where T: Codable, T: Equatable { // MARK: Properties /// Possible hints from the search query. - public var hits: [T] = [] + public var hits: [SearchHit] = [] /// Distribution of the given facets. public var facetDistribution: [String: [String: Int]]? @@ -34,6 +34,37 @@ public class Searchable: Equatable, Codable where T: Codable, T: Equatable { } } +@dynamicMemberLookup +public struct SearchHit: Equatable, Codable where T: Codable, T: Equatable { + public let document: T + public internal(set) var rankingScore: Double? + + /// Dynamic member lookup is used to allow easy access to instance members of the hit result, maintaining a level of backwards compatibility. + public subscript(dynamicMember keyPath: KeyPath) -> V { + document[keyPath: keyPath] + } + + // MARK: Codable + + enum CodingKeys: String, CodingKey { + case rankingScore = "_rankingScore" + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.document = try T(from: decoder) + self.rankingScore = try container.decodeIfPresent(Double.self, forKey: .rankingScore) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(document) + + var containerTwo = encoder.container(keyedBy: CodingKeys.self) + try containerTwo.encodeIfPresent(rankingScore, forKey: .rankingScore) + } +} + /** `SearchResult` instances represent the result of a search. Requires that the value `T` conforms to the `Codable` and `Equatable` protocols. diff --git a/Sources/MeiliSearch/Model/Setting.swift b/Sources/MeiliSearch/Model/Setting.swift index a5fe4b04..7d46daba 100644 --- a/Sources/MeiliSearch/Model/Setting.swift +++ b/Sources/MeiliSearch/Model/Setting.swift @@ -4,7 +4,7 @@ import Foundation #endif /** - Settings object provided byb the user + Settings object provided by the user */ public struct Setting: Codable, Equatable { // MARK: Properties @@ -33,6 +33,9 @@ public struct Setting: Codable, Equatable { /// List of attributes used for sorting public let sortableAttributes: [String]? + /// Settings for typo tolerance + public let typoTolerance: TypoTolerance? + /// List of tokens that will be considered as word separators by Meilisearch. public let separatorTokens: [String]? @@ -59,7 +62,8 @@ public struct Setting: Codable, Equatable { separatorTokens: [String]? = nil, nonSeparatorTokens: [String]? = nil, dictionary: [String]? = nil, - pagination: Pagination? = nil + pagination: Pagination? = nil, + typoTolerance: TypoTolerance? = nil ) { self.rankingRules = rankingRules self.searchableAttributes = searchableAttributes @@ -73,5 +77,6 @@ public struct Setting: Codable, Equatable { self.separatorTokens = separatorTokens self.dictionary = dictionary self.pagination = pagination + self.typoTolerance = typoTolerance } } diff --git a/Sources/MeiliSearch/Model/SettingResult.swift b/Sources/MeiliSearch/Model/SettingResult.swift index 2ce7fcc1..ae5efecf 100644 --- a/Sources/MeiliSearch/Model/SettingResult.swift +++ b/Sources/MeiliSearch/Model/SettingResult.swift @@ -39,4 +39,7 @@ public struct SettingResult: Codable, Equatable { /// Pagination settings for the current index public let pagination: Pagination + + /// Settings for typo tolerance + public let typoTolerance: TypoToleranceResult } diff --git a/Sources/MeiliSearch/Model/Task.swift b/Sources/MeiliSearch/Model/Task.swift deleted file mode 100644 index 476c87d3..00000000 --- a/Sources/MeiliSearch/Model/Task.swift +++ /dev/null @@ -1,129 +0,0 @@ -import Foundation -#if canImport(FoundationNetworking) - import FoundationNetworking -#endif - -/** - `Task` instances represent the current transaction status, use the `uid` value to - verify the status of your transaction. - */ -public struct Task: Codable, Equatable { - - // MARK: Properties - - /// Unique ID for the current `Task`. - public let uid: Int - - /// Unique ID for the current `Task`. - public let indexUid: String - - /// Returns if the task has been successful or not. - public let status: Task.Status - - /// Type of the task. - public let type: String - - /// Details of the task. - public let details: Details? - - /// Duration of the task process. - public let duration: String? - - /// Date when the task has been enqueued. - public let enqueuedAt: String - - /// Date when the task has been processed. - public let processedAt: Date? - - /// Type of `Task`. - public struct Details: Codable, Equatable { - - // MARK: Properties - - /// Number of documents sent - public let receivedDocuments: Int? - - /// Number of documents successfully indexed/updated in Meilisearch - public let indexedDocuments: Int? - - /// Number of deleted documents - public let deletedDocuments: Int? - - /// Primary key on index creation - public let primaryKey: String? - - /// Ranking rules on settings actions - public let rankingRules: [String]? - - /// Searchable attributes on settings actions - public let searchableAttributes: [String]? - - /// Displayed attributes on settings actions - public let displayedAttributes: [String]? - - /// Filterable attributes on settings actions - public let filterableAttributes: [String]? - - /// Sortable attributes on settings actions - public let sortableAttributes: [String]? - - /// Stop words on settings actions - public let stopWords: [String]? - - /// Synonyms on settings actions - public let synonyms: [String: [String]]? - - /// Distinct attribute on settings actions - public let distinctAttribute: String? - - /// List of tokens that will be considered as word separators by Meilisearch. - public let separatorTokens: [String]? - - /// List of tokens that will not be considered as word separators by Meilisearch. - public let nonSeparatorTokens: [String]? - - /// List of words on which the segmentation will be overridden. - public let dictionary: [String]? - - /// Settings for index level pagination rules - public let pagination: Pagination? - - } - /// Error information in case of failed update. - public let error: MeiliSearch.MSErrorResponse? - - public enum Status: Codable, Equatable { - /// When a task was successfully enqueued and is waiting to be processed. - case enqueued - /// When a task is still being processed. - case processing - /// When a task was successfully processed. - case succeeded - /// When a task had an error and could not be completed for some reason. - case failed - - public enum StatusError: Error { - case unknown - } - - public init(from decoder: Decoder) throws { - let container: SingleValueDecodingContainer = try decoder.singleValueContainer() - let rawStatus: String = try container.decode(String.self) - - switch rawStatus { - case "enqueued": - self = Status.enqueued - case "processing": - self = Status.processing - case "succeeded": - self = Status.succeeded - case "failed": - self = Status.failed - default: - throw StatusError.unknown - } - } - - public func encode(to encoder: Encoder) throws { } - } -} diff --git a/Sources/MeiliSearch/Model/Task/Task.swift b/Sources/MeiliSearch/Model/Task/Task.swift new file mode 100644 index 00000000..8cadacb1 --- /dev/null +++ b/Sources/MeiliSearch/Model/Task/Task.swift @@ -0,0 +1,65 @@ +import Foundation + +/** + `Task` instances represent the current transaction status, use the `uid` value to + verify the status of your transaction. + */ +public struct Task: Decodable, Equatable { + /// Unique ID for the current `Task`. + public let uid: Int + + /// Unique identifier of the targeted index + public let indexUid: String? + + /// Returns if the task has been successful or not. + public let status: Status + + /// Type of the task. + public let type: TaskType + + /// Details of the task. + public let details: Details? + + /// Duration of the task process. + public let duration: String? + + /// Date when the task has been enqueued. + public let enqueuedAt: Date + + /// Date when the task has been started. + public let startedAt: Date? + + /// Date when the task has been finished, regardless of status. + public let finishedAt: Date? + + /// ID of the the `Task` which caused this to be canceled. + public let canceledBy: Int? + + /// Error information in case of failed update. + public let error: MeiliSearch.MSErrorResponse? + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let type = try container.decode(TaskType.self, forKey: .type) + + self.uid = try container.decode(Int.self, forKey: .uid) + self.indexUid = try container.decodeIfPresent(String.self, forKey: .indexUid) + self.status = try container.decode(Status.self, forKey: .status) + self.type = type + self.duration = try container.decodeIfPresent(String.self, forKey: .duration) + self.enqueuedAt = try container.decode(Date.self, forKey: .enqueuedAt) + self.startedAt = try container.decodeIfPresent(Date.self, forKey: .startedAt) + self.finishedAt = try container.decodeIfPresent(Date.self, forKey: .finishedAt) + self.canceledBy = try container.decodeIfPresent(Int.self, forKey: .canceledBy) + self.error = try container.decodeIfPresent(MeiliSearch.MSErrorResponse.self, forKey: .error) + + // we ignore errors thrown by `superDecoder` to handle cases where no details are provided by the API + // for example when the type is `snapshotCreation`. + let detailsDecoder = try? container.superDecoder(forKey: .details) + self.details = try Details(decoder: detailsDecoder, type: type) + } + + enum CodingKeys: String, CodingKey { + case uid, indexUid, status, type, details, duration, enqueuedAt, startedAt, finishedAt, canceledBy, error + } +} diff --git a/Sources/MeiliSearch/Model/Task/TaskDetails.swift b/Sources/MeiliSearch/Model/Task/TaskDetails.swift new file mode 100644 index 00000000..571a16f7 --- /dev/null +++ b/Sources/MeiliSearch/Model/Task/TaskDetails.swift @@ -0,0 +1,110 @@ +import Foundation + +public extension Task { + enum Details: Equatable { + case indexCreation(TaskIndexCreationDetails) + case indexUpdate(TaskIndexUpdateDetails) + case indexDeletion(TaskIndexDeletionDetails) + case indexSwap(TaskIndexSwapDetails) + case documentAdditionOrUpdate(TaskDocumentAdditionOrUpdateDetails) + case documentDeletion(TaskDocumentDeletionDetails) + case settingsUpdate(Setting) + case dumpCreation(TaskDumpCreationDetails) + case taskCancelation(TaskCancellationDetails) + case taskDeletion(TaskDeletionDetails) + + init?(decoder: Decoder?, type: TaskType) throws { + guard let decoder = decoder else { + return nil + } + + switch type { + case .indexCreation: + self = try .indexCreation(.init(from: decoder)) + case .indexUpdate: + self = try .indexUpdate(.init(from: decoder)) + case .indexDeletion: + self = try .indexDeletion(.init(from: decoder)) + case .indexSwap: + self = try .indexSwap(.init(from: decoder)) + case .documentAdditionOrUpdate: + self = try .documentAdditionOrUpdate(.init(from: decoder)) + case .documentDeletion: + self = try .documentDeletion(.init(from: decoder)) + case .settingsUpdate: + self = try .settingsUpdate(.init(from: decoder)) + case .dumpCreation: + self = try .dumpCreation(.init(from: decoder)) + case .taskCancelation: + self = try .taskCancelation(.init(from: decoder)) + case .taskDeletion: + self = try .taskDeletion(.init(from: decoder)) + case .snapshotCreation: + // as per documentation: "The details object is set to null for snapshotCreation tasks." + return nil + case .unknown: + // we do not handle this type, and as such even if details exist we do not support them here + return nil + } + } + } + + struct TaskDocumentAdditionOrUpdateDetails: Decodable, Equatable { + /// Number of documents sent + public let receivedDocuments: Int? + + /// Number of documents successfully indexed/updated in Meilisearch + public let indexedDocuments: Int? + } + + struct TaskDocumentDeletionDetails: Decodable, Equatable { + /// Number of documents queued for deletion + public let providedIds: Int? + /// The filter used to delete documents. nil if it was not specified + public let originalFilter: String? + /// Number of documents deleted. nil while the task status is enqueued or processing + public let deletedDocuments: Int? + } + + struct TaskIndexCreationDetails: Decodable, Equatable { + /// Value of the primaryKey field supplied during index creation. nil if it was not specified + public let primaryKey: String? + } + + struct TaskIndexUpdateDetails: Decodable, Equatable { + /// Value of the primaryKey field supplied during index update. nil if it was not specified + public let primaryKey: String? + } + + struct TaskIndexDeletionDetails: Decodable, Equatable { + /// Number of deleted documents. This should equal the total number of documents in the deleted index. nil while the task status is enqueued or processing + public let deletedDocuments: Int? + } + + struct TaskIndexSwapDetails: Decodable, Equatable { + // To be populated under https://github.com/meilisearch/meilisearch-swift/issues/367 + } + + struct TaskCancellationDetails: Decodable, Equatable { + /// The number of matched tasks. If the API key used for the request doesn’t have access to an index, tasks relating to that index will not be included in matchedTasks + public let matchedTasks: Int? + /// The number of tasks successfully canceled. If the task cancelation fails, this will be 0. nil when the task status is enqueued or processing + public let canceledTasks: Int? + /// The filter used in the cancel task request + public let originalFilter: String? + } + + struct TaskDeletionDetails: Decodable, Equatable { + /// The number of matched tasks. If the API key used for the request doesn’t have access to an index, tasks relating to that index will not be included in matchedTasks + public let matchedTasks: Int? + /// The number of tasks successfully deleted. If the task deletion fails, this will be 0. nil when the task status is enqueued or processing + public let deletedTasks: Int? + /// The filter used in the delete task request + public let originalFilter: String? + } + + struct TaskDumpCreationDetails: Decodable, Equatable { + /// The generated uid of the dump. This is also the name of the generated dump file. nil when the task status is enqueued, processing, canceled, or failed + public let dumpUid: String? + } +} diff --git a/Sources/MeiliSearch/Model/TaskInfo.swift b/Sources/MeiliSearch/Model/Task/TaskInfo.swift similarity index 88% rename from Sources/MeiliSearch/Model/TaskInfo.swift rename to Sources/MeiliSearch/Model/Task/TaskInfo.swift index d64c72a9..70d6af1c 100644 --- a/Sources/MeiliSearch/Model/TaskInfo.swift +++ b/Sources/MeiliSearch/Model/Task/TaskInfo.swift @@ -5,9 +5,6 @@ import Foundation verify the status of your transaction. */ public struct TaskInfo: Codable, Equatable { - - // MARK: Properties - /// Unique ID for the current `TaskInfo`. public let taskUid: Int @@ -18,10 +15,10 @@ public struct TaskInfo: Codable, Equatable { public let status: Task.Status /// Type of the task. - public let type: String + public let type: TaskType /// Date when the task has been enqueued. - public let enqueuedAt: String + public let enqueuedAt: Date public enum CodingKeys: String, CodingKey { case taskUid, indexUid, status, type, enqueuedAt diff --git a/Sources/MeiliSearch/Model/Task/TaskStatus.swift b/Sources/MeiliSearch/Model/Task/TaskStatus.swift new file mode 100644 index 00000000..b55f2dc3 --- /dev/null +++ b/Sources/MeiliSearch/Model/Task/TaskStatus.swift @@ -0,0 +1,45 @@ +import Foundation + +public extension Task { + enum Status: String, Codable, Equatable { + /// When a task was successfully enqueued and is waiting to be processed. + case enqueued + /// When a task is still being processed. + case processing + /// When a task was successfully processed. + case succeeded + /// When a task had an error and could not be completed for some reason. + case failed + /// When a task has been canceled. + case canceled + + public enum StatusError: Error { + case unknown + } + + public init(from decoder: Decoder) throws { + let container: SingleValueDecodingContainer = try decoder.singleValueContainer() + let rawStatus: String = try container.decode(String.self) + + switch rawStatus { + case "enqueued": + self = Status.enqueued + case "processing": + self = Status.processing + case "succeeded": + self = Status.succeeded + case "failed": + self = Status.failed + case "canceled": + self = Status.canceled + default: + throw StatusError.unknown + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(rawValue) + } + } +} diff --git a/Sources/MeiliSearch/Model/Task/TaskType.swift b/Sources/MeiliSearch/Model/Task/TaskType.swift new file mode 100644 index 00000000..48c3f680 --- /dev/null +++ b/Sources/MeiliSearch/Model/Task/TaskType.swift @@ -0,0 +1,97 @@ +import Foundation + +/** + `TaskType`defines the possible types of task which could be returned by the `/tasks` API + */ +public enum TaskType: Codable, Equatable, LosslessStringConvertible { + case indexCreation + case indexUpdate + case indexDeletion + case indexSwap + case documentAdditionOrUpdate + case documentDeletion + case settingsUpdate + case dumpCreation + case taskCancelation + case taskDeletion + case snapshotCreation + + // Captures task types which are not currently known by the Swift package. + // This allows for a future proofing should the MeiliSearch executable introduce new values. + case unknown(String) + + public init(from decoder: Decoder) throws { + let value = try decoder.singleValueContainer().decode(String.self) + self.init(rawValue: value) + } + + internal init(rawValue: String) { + switch rawValue { + case "indexCreation": + self = .indexCreation + case "indexUpdate": + self = .indexUpdate + case "indexDeletion": + self = .indexDeletion + case "indexSwap": + self = .indexSwap + case "documentAdditionOrUpdate": + self = .documentAdditionOrUpdate + case "documentDeletion": + self = .documentDeletion + case "settingsUpdate": + self = .settingsUpdate + case "dumpCreation": + self = .dumpCreation + case "taskCancelation": + self = .taskCancelation + case "taskDeletion": + self = .taskDeletion + case "snapshotCreation": + self = .snapshotCreation + default: + self = .unknown(rawValue) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(description) + } + + // MARK: LosslessStringConvertible + // ensures that when TaskType is converted to String, the "unknown" context is ignored + + public init?(_ description: String) { + self.init(rawValue: description) + } + + public var description: String { + switch self { + case .indexCreation: + return "indexCreation" + case .indexUpdate: + return "indexUpdate" + case .indexDeletion: + return "indexDeletion" + case .indexSwap: + return "indexSwap" + case .documentAdditionOrUpdate: + return "documentAdditionOrUpdate" + case .documentDeletion: + return "documentDeletion" + case .settingsUpdate: + return "settingsUpdate" + case .dumpCreation: + return "dumpCreation" + case .taskCancelation: + return "taskCancelation" + case .taskDeletion: + return "taskDeletion" + case .snapshotCreation: + return "snapshotCreation" + case .unknown(let unknownTaskTypeValue): + return unknownTaskTypeValue + } + } +} diff --git a/Sources/MeiliSearch/Model/TasksResults.swift b/Sources/MeiliSearch/Model/Task/TasksResults.swift similarity index 77% rename from Sources/MeiliSearch/Model/TasksResults.swift rename to Sources/MeiliSearch/Model/Task/TasksResults.swift index aaca9ecc..64e039b2 100644 --- a/Sources/MeiliSearch/Model/TasksResults.swift +++ b/Sources/MeiliSearch/Model/Task/TasksResults.swift @@ -3,8 +3,7 @@ import Foundation /** `TasksResults` is a wrapper used in the tasks routes to handle the returned data. */ - -public struct TasksResults: Codable, Equatable { +public struct TasksResults: Decodable, Equatable { /// Results list containing objects of `Task`. public let results: [Task] /// Integer value used to retrieve the next batch of tasks. @@ -13,4 +12,6 @@ public struct TasksResults: Codable, Equatable { public let from: Int? /// Max number of records to be returned in one request. public let limit: Int + /// Total number of tasks matching the filter or query + public let total: Int } diff --git a/Sources/MeiliSearch/Model/TypoTolerance.swift b/Sources/MeiliSearch/Model/TypoTolerance.swift new file mode 100644 index 00000000..c11b0ef4 --- /dev/null +++ b/Sources/MeiliSearch/Model/TypoTolerance.swift @@ -0,0 +1,48 @@ +import Foundation + +/** + `TypoTolerance` settings provided by the user. + */ +public struct TypoTolerance: Codable, Equatable { + // MARK: Properties + + /// Whether typo tolerance is enabled or not + public let enabled: Bool? + + /// The minimum word size for accepting typos + public let minWordSizeForTypos: MinWordSize? + + /// An array of words for which the typo tolerance feature is disabled + public let disableOnWords: [String]? + + /// An array of attributes for which the typo tolerance feature is disabled + public let disableOnAttributes: [String]? + + public struct MinWordSize: Codable, Equatable { + /// The minimum word size for accepting 1 typo; must be between 0 and `twoTypos` + public let oneTypo: Int? + + /// The minimum word size for accepting 2 typos; must be between `oneTypo` and 255 + public let twoTypos: Int? + + public init( + oneTypo: Int? = nil, + twoTypos: Int? = nil + ) { + self.oneTypo = oneTypo + self.twoTypos = twoTypos + } + } + + public init( + enabled: Bool? = nil, + minWordSizeForTypos: TypoTolerance.MinWordSize? = nil, + disableOnWords: [String]? = nil, + disableOnAttributes: [String]? = nil + ) { + self.enabled = enabled + self.minWordSizeForTypos = minWordSizeForTypos + self.disableOnWords = disableOnWords + self.disableOnAttributes = disableOnAttributes + } +} diff --git a/Sources/MeiliSearch/Model/TypoToleranceResult.swift b/Sources/MeiliSearch/Model/TypoToleranceResult.swift new file mode 100644 index 00000000..0a849e73 --- /dev/null +++ b/Sources/MeiliSearch/Model/TypoToleranceResult.swift @@ -0,0 +1,28 @@ +import Foundation + +/** + `TypoToleranceResult` instances represent the current typo tolerance settings. + */ +public struct TypoToleranceResult: Codable, Equatable { + // MARK: Properties + + /// Whether typo tolerance is enabled or not + public let enabled: Bool + + /// The minimum word size for accepting typos + public let minWordSizeForTypos: MinWordSize + + /// An array of words for which the typo tolerance feature is disabled + public let disableOnWords: [String] + + /// An array of attributes for which the typo tolerance feature is disabled + public let disableOnAttributes: [String] + + public struct MinWordSize: Codable, Equatable { + /// The minimum word size for accepting 1 typo; must be between 0 and `twoTypos` + public let oneTypo: Int + + /// The minimum word size for accepting 2 typos; must be between `oneTypo` and 255 + public let twoTypos: Int + } +} diff --git a/Sources/MeiliSearch/QueryParameters/CancelTasksQuery.swift b/Sources/MeiliSearch/QueryParameters/CancelTasksQuery.swift new file mode 100644 index 00000000..4cef573c --- /dev/null +++ b/Sources/MeiliSearch/QueryParameters/CancelTasksQuery.swift @@ -0,0 +1,51 @@ +import Foundation + +/** + `CancelTasksQuery` class represent the options used to filter a cancel tasks call. + */ +public class CancelTasksQuery: Queryable { + /// List of strings with all the types the response should contain. + public let types: [TaskType] + /// List of strings with all the statuses the response should contain. + public let statuses: [Task.Status] + /// Filter tasks response by a particular list of index Uids strings + public let indexUids: [String] + /// Filter tasks based on a list of task's uids. + public let uids: [Int] + /// Filter tasks based on the date before the task were enqueued at. + public let beforeEnqueuedAt: Date? + /// Filter tasks based on the date after the task were enqueued at. + public let afterEnqueuedAt: Date? + /// Filter tasks based on the date before the task were started. + public let beforeStartedAt: Date? + /// Filter tasks based on the date after the task were started at. + public let afterStartedAt: Date? + + init( + types: [TaskType]? = nil, statuses: [Task.Status]? = nil, + indexUids: [String]? = nil, uids: [Int]? = nil, + beforeEnqueuedAt: Date? = nil, afterEnqueuedAt: Date? = nil, + beforeStartedAt: Date? = nil, afterStartedAt: Date? = nil + ) { + self.types = types ?? [] + self.statuses = statuses ?? [] + self.indexUids = indexUids ?? [] + self.uids = uids ?? [] + self.beforeEnqueuedAt = beforeEnqueuedAt + self.afterEnqueuedAt = afterEnqueuedAt + self.beforeStartedAt = beforeStartedAt + self.afterStartedAt = afterStartedAt + } + + internal func buildQuery() -> [String: Codable?] { + [ + "uids": uids.isEmpty ? nil : uids.map(String.init).joined(separator: ","), + "types": types.isEmpty ? nil : types.map({ $0.description }).joined(separator: ","), + "statuses": statuses.isEmpty ? nil : statuses.map({ $0.rawValue }).joined(separator: ","), + "indexUids": indexUids.isEmpty ? nil : indexUids.joined(separator: ","), + "beforeEnqueuedAt": Formatter.formatOptionalDate(date: beforeEnqueuedAt), + "afterEnqueuedAt": Formatter.formatOptionalDate(date: afterEnqueuedAt), + "beforeStartedAt": Formatter.formatOptionalDate(date: beforeStartedAt), + ] + } +} diff --git a/Sources/MeiliSearch/QueryParameters/DeleteTasksQuery.swift b/Sources/MeiliSearch/QueryParameters/DeleteTasksQuery.swift new file mode 100644 index 00000000..b720bc06 --- /dev/null +++ b/Sources/MeiliSearch/QueryParameters/DeleteTasksQuery.swift @@ -0,0 +1,64 @@ +import Foundation + +/** + `DeleteTasksQuery` class represent the options used to filter a delete tasks call. + */ +public class DeleteTasksQuery: Queryable { + /// List of strings with all the types the response should contain. + public let types: [TaskType] + /// List of strings with all the statuses the response should contain. + public let statuses: [Task.Status] + /// Filter tasks response by a particular list of index Uids strings + public let indexUids: [String] + /// Filter tasks based on a list of task's uids. + public let uids: [Int] + /// Filter tasks based on a list of task's uids which were used to cancel other tasks. + public let canceledBy: [Int] + /// Filter tasks based on the date before the task were enqueued at. + public let beforeEnqueuedAt: Date? + /// Filter tasks based on the date after the task were enqueued at. + public let afterEnqueuedAt: Date? + /// Filter tasks based on the date before the task were started. + public let beforeStartedAt: Date? + /// Filter tasks based on the date after the task were started at. + public let afterStartedAt: Date? + /// Filter tasks based on the date before the task was finished. + public let beforeFinishedAt: Date? + /// Filter tasks based on the date after the task was finished. + public let afterFinishedAt: Date? + + init( + statuses: [Task.Status]? = nil, types: [TaskType]? = nil, + indexUids: [String]? = nil, uids: [Int]? = nil, canceledBy: [Int]? = nil, + beforeEnqueuedAt: Date? = nil, afterEnqueuedAt: Date? = nil, + beforeStartedAt: Date? = nil, afterStartedAt: Date? = nil, + beforeFinishedAt: Date? = nil, afterFinishedAt: Date? = nil + ) { + self.statuses = statuses ?? [] + self.types = types ?? [] + self.indexUids = indexUids ?? [] + self.uids = uids ?? [] + self.canceledBy = canceledBy ?? [] + self.beforeEnqueuedAt = beforeEnqueuedAt + self.afterEnqueuedAt = afterEnqueuedAt + self.beforeStartedAt = beforeStartedAt + self.afterStartedAt = afterStartedAt + self.beforeFinishedAt = beforeFinishedAt + self.afterFinishedAt = afterFinishedAt + } + + internal func buildQuery() -> [String: Codable?] { + [ + "uids": uids.isEmpty ? nil : uids.map(String.init).joined(separator: ","), + "types": types.isEmpty ? nil : types.map({ $0.description }).joined(separator: ","), + "statuses": statuses.isEmpty ? nil : statuses.map({ $0.rawValue }).joined(separator: ","), + "indexUids": indexUids.isEmpty ? nil : indexUids.joined(separator: ","), + "canceledBy": canceledBy.isEmpty ? nil : canceledBy.map(String.init).joined(separator: ","), + "beforeEnqueuedAt": Formatter.formatOptionalDate(date: beforeEnqueuedAt), + "afterEnqueuedAt": Formatter.formatOptionalDate(date: afterEnqueuedAt), + "beforeStartedAt": Formatter.formatOptionalDate(date: beforeStartedAt), + "beforeFinishedAt": Formatter.formatOptionalDate(date: beforeFinishedAt), + "afterFinishedAt": Formatter.formatOptionalDate(date: afterFinishedAt), + ] + } +} diff --git a/Sources/MeiliSearch/QueryParameters/TasksQuery.swift b/Sources/MeiliSearch/QueryParameters/TasksQuery.swift index 7e5980de..d8eda862 100644 --- a/Sources/MeiliSearch/QueryParameters/TasksQuery.swift +++ b/Sources/MeiliSearch/QueryParameters/TasksQuery.swift @@ -13,9 +13,9 @@ public class TasksQuery: Queryable { /// Integer value used to retrieve the next batch of tasks. private var next: Int? /// List of strings with all the types the response should contain. - private var types: [String] + private var types: [TaskType] /// List of strings with all the statuses the response should contain. - private var statuses: [String] + private var statuses: [Task.Status] /// Filter tasks response by a particular list of index Uids strings var indexUids: [String] /// Filter tasks based on a list of task's uids. @@ -37,7 +37,7 @@ public class TasksQuery: Queryable { init( limit: Int? = nil, from: Int? = nil, next: Int? = nil, - statuses: [String]? = nil, types: [String]? = nil, + statuses: [Task.Status]? = nil, types: [TaskType]? = nil, indexUids: [String]? = nil, uids: [Int]? = nil, canceledBy: [Int]? = nil, beforeEnqueuedAt: Date? = nil, afterEnqueuedAt: Date? = nil, afterFinishedAt: Date? = nil, beforeStartedAt: Date? = nil, @@ -65,8 +65,8 @@ public class TasksQuery: Queryable { "from": from, "next": next, "uids": uids.isEmpty ? nil : uids.map(String.init).joined(separator: ","), - "types": types.isEmpty ? nil : types.joined(separator: ","), - "statuses": statuses.isEmpty ? nil : statuses.joined(separator: ","), + "types": types.isEmpty ? nil : types.map({ $0.description }).joined(separator: ","), + "statuses": statuses.isEmpty ? nil : statuses.map({ $0.rawValue }).joined(separator: ","), "indexUids": indexUids.isEmpty ? nil : indexUids.joined(separator: ","), "canceledBy": canceledBy.isEmpty ? nil : canceledBy.map(String.init).joined(separator: ","), "beforeEnqueuedAt": Formatter.formatOptionalDate(date: beforeEnqueuedAt), diff --git a/Sources/MeiliSearch/Request.swift b/Sources/MeiliSearch/Request.swift index bb73d4cc..9b363575 100755 --- a/Sources/MeiliSearch/Request.swift +++ b/Sources/MeiliSearch/Request.swift @@ -96,10 +96,15 @@ public final class Request { func post( api: String, + param: String? = nil, headers: [String: String] = [:], - _ data: Data, + _ data: Data?, _ completion: @escaping (Result) -> Void) { - guard let url = URL(string: config.url(api: api)) else { + var urlString: String = config.url(api: api) + if let param: String = param, !param.isEmpty { + urlString += param + } + guard let url = URL(string: urlString) else { completion(.failure(MeiliSearch.Error.invalidURL())) return } @@ -178,9 +183,14 @@ public final class Request { func delete( api: String, + param: String? = nil, headers: [String: String] = [:], _ completion: @escaping (Result) -> Void) { - guard let url = URL(string: config.url(api: api)) else { + var urlString: String = config.url(api: api) + if let param: String = param, !param.isEmpty { + urlString += param + } + guard let url = URL(string: urlString) else { completion(.failure(MeiliSearch.Error.invalidURL())) return } diff --git a/Sources/MeiliSearch/Settings.swift b/Sources/MeiliSearch/Settings.swift index 92c1937c..58b4bfe4 100644 --- a/Sources/MeiliSearch/Settings.swift +++ b/Sources/MeiliSearch/Settings.swift @@ -372,6 +372,51 @@ struct Settings { resetSetting(uid: uid, key: "pagination", completion: completion) } + // MARK: Typo Tolerance + + func getTypoTolerance( + _ uid: String, + _ completion: @escaping (Result) -> Void) { + + getSetting(uid: uid, key: "typo-tolerance", completion: completion) + } + + func updateTypoTolerance( + _ uid: String, + _ setting: TypoTolerance, + _ completion: @escaping (Result) -> Void) { + + let data: Data + do { + data = try JSONEncoder().encode(setting) + } catch { + completion(.failure(error)) + return + } + + // this uses patch instead of put for networking, so shouldn't use the reusable 'updateSetting' function + self.request.patch(api: "/indexes/\(uid)/settings/typo-tolerance", data) { result in + switch result { + case .success(let data): + do { + let task: TaskInfo = try Constants.customJSONDecoder.decode(TaskInfo.self, from: data) + completion(.success(task)) + } catch { + completion(.failure(error)) + } + case .failure(let error): + completion(.failure(error)) + } + } + } + + func resetTypoTolerance( + _ uid: String, + _ completion: @escaping (Result) -> Void) { + + resetSetting(uid: uid, key: "typo-tolerance", completion: completion) + } + // MARK: Reusable Requests private func getSetting( diff --git a/Sources/MeiliSearch/Tasks.swift b/Sources/MeiliSearch/Tasks.swift index a226fd7e..624884c9 100644 --- a/Sources/MeiliSearch/Tasks.swift +++ b/Sources/MeiliSearch/Tasks.swift @@ -136,4 +136,44 @@ struct Tasks { } } } + + // MARK: Cancel Tasks + + func cancelTasks( + _ params: CancelTasksQuery, + _ completion: @escaping (Result) -> Void) { + self.request.post(api: "/tasks/cancel", param: params.toQuery(), nil) { result in + switch result { + case .success(let data): + do { + let task: Result = try Constants.resultDecoder(data: data) + completion(task) + } catch { + completion(.failure(error)) + } + case .failure(let error): + completion(.failure(error)) + } + } + } + + // MARK: Delete Tasks + + func deleteTasks( + _ params: DeleteTasksQuery, + _ completion: @escaping (Result) -> Void) { + self.request.delete(api: "/tasks", param: params.toQuery()) { result in + switch result { + case .success(let data): + do { + let task: Result = try Constants.resultDecoder(data: data) + completion(task) + } catch { + completion(.failure(error)) + } + case .failure(let error): + completion(.failure(error)) + } + } + } } diff --git a/Tests/MeiliSearchIntegrationTests/DocumentsTests.swift b/Tests/MeiliSearchIntegrationTests/DocumentsTests.swift index fd71110a..ff95cb18 100755 --- a/Tests/MeiliSearchIntegrationTests/DocumentsTests.swift +++ b/Tests/MeiliSearchIntegrationTests/DocumentsTests.swift @@ -6,7 +6,7 @@ import Foundation #endif // swiftlint:disable force_unwrapping -// swiftlint:disable force_try + private let movies: [Movie] = [ Movie(id: 123, title: "Pride and Prejudice", comment: "A great book"), Movie(id: 456, title: "Le Petit Prince", comment: "A french book"), @@ -24,10 +24,10 @@ class DocumentsTests: XCTestCase { private var session: URLSessionProtocol! private let uid: String = "books_test" - override func setUp() { - super.setUp() + override func setUpWithError() throws { + try super.setUpWithError() session = URLSession(configuration: .ephemeral) - client = try! MeiliSearch(host: currentHost(), apiKey: "masterKey", session: session) + client = try MeiliSearch(host: currentHost(), apiKey: "masterKey", session: session) index = self.client.index(self.uid) let expectation = XCTestExpectation(description: "Create index if it does not exist") self.client.createIndex(uid: uid) { result in @@ -63,26 +63,13 @@ class DocumentsTests: XCTestCase { self.client.waitForTask(task: task) { result in switch result { case .success(let task): - XCTAssertEqual("documentAdditionOrUpdate", task.type) + XCTAssertEqual("documentAdditionOrUpdate", task.type.description) XCTAssertEqual(Task.Status.succeeded, task.status) - if let details = task.details { - if let indexedDocuments = details.indexedDocuments { - XCTAssertEqual(8, indexedDocuments) - } else { - XCTFail("IndexedDocuments field should not be nil") - } + if case .documentAdditionOrUpdate(let details) = task.details { + XCTAssertEqual(8, details.indexedDocuments) + XCTAssertEqual(8, details.receivedDocuments) } else { - XCTFail("IndexedDocuments field should exists in details field of task") - } - - if let details = task.details { - if let receivedDocuments = details.receivedDocuments { - XCTAssertEqual(8, receivedDocuments) - } else { - XCTFail("receivedDocuments field should not be nil") - } - } else { - XCTFail("receivedDocuments field should exists in details field of task") + XCTFail("documentAdditionOrUpdate details should be set by task") } expectation.fulfill() case .failure(let error): @@ -111,7 +98,7 @@ class DocumentsTests: XCTestCase { self.client.waitForTask(task: task) { result in switch result { case .success(let task): - XCTAssertEqual("documentAdditionOrUpdate", task.type) + XCTAssertEqual("documentAdditionOrUpdate", task.type.description) XCTAssertEqual(Task.Status.succeeded, task.status) expectation.fulfill() case .failure(let error): @@ -143,7 +130,7 @@ class DocumentsTests: XCTestCase { switch result { case .success(let task): XCTAssertEqual(Task.Status.succeeded, task.status) - XCTAssertEqual("documentAdditionOrUpdate", task.type) + XCTAssertEqual("documentAdditionOrUpdate", task.type.description) self.index.getDocuments(params: DocumentsQuery(limit: 1, offset: 1, fields: ["id", "title"])) { (result: Result, Swift.Error>) in switch result { @@ -189,9 +176,9 @@ class DocumentsTests: XCTestCase { self.wait(for: [expectation], timeout: 3.0) } - func testAddAndGetOneDocumentWithIntIdentifierAndSucceed() { + func testAddAndGetOneDocumentWithIntIdentifierAndSucceed() throws { let movie = Movie(id: 10, title: "test", comment: "test movie") - let documents: Data = try! JSONEncoder().encode([movie]) + let documents: Data = try JSONEncoder().encode([movie]) let expectation = XCTestExpectation(description: "Add or replace Movies document") self.index.addDocuments( @@ -204,7 +191,7 @@ class DocumentsTests: XCTestCase { switch result { case .success(let task): XCTAssertEqual(Task.Status.succeeded, task.status) - XCTAssertEqual("documentAdditionOrUpdate", task.type) + XCTAssertEqual("documentAdditionOrUpdate", task.type.description) self.index.getDocument(10 ) { (result: Result) in switch result { @@ -232,9 +219,9 @@ class DocumentsTests: XCTestCase { self.wait(for: [expectation], timeout: TESTS_TIME_OUT) } - func testAddAndGetOneDocument() { + func testAddAndGetOneDocument() throws { let movie = Movie(id: 10, title: "test", comment: "test movie") - let documents: Data = try! JSONEncoder().encode([movie]) + let documents: Data = try JSONEncoder().encode([movie]) let expectation = XCTestExpectation(description: "Add or replace Movies document") self.index.addDocuments( @@ -247,7 +234,7 @@ class DocumentsTests: XCTestCase { switch result { case .success(let task): XCTAssertEqual(Task.Status.succeeded, task.status) - XCTAssertEqual("documentAdditionOrUpdate", task.type) + XCTAssertEqual("documentAdditionOrUpdate", task.type.description) self.index.getDocument("10" ) { (result: Result) in switch result { @@ -275,10 +262,10 @@ class DocumentsTests: XCTestCase { self.wait(for: [expectation], timeout: TESTS_TIME_OUT) } - func testUpdateDocument() { + func testUpdateDocument() throws { let identifier: Int = 1844 let movie: Movie = movies.first(where: { (movie: Movie) in movie.id == identifier })! - let documents: Data = try! JSONEncoder().encode([movie]) + let documents: Data = try JSONEncoder().encode([movie]) let expectation = XCTestExpectation(description: "Add or update Movies document") @@ -292,7 +279,7 @@ class DocumentsTests: XCTestCase { switch result { case .success(let task): XCTAssertEqual(Task.Status.succeeded, task.status) - XCTAssertEqual("documentAdditionOrUpdate", task.type) + XCTAssertEqual("documentAdditionOrUpdate", task.type.description) expectation.fulfill() case .failure: XCTFail("Failed to wait for task") @@ -309,8 +296,8 @@ class DocumentsTests: XCTestCase { self.wait(for: [expectation], timeout: TESTS_TIME_OUT) } - func testDeleteOneDocument() { - let documents: Data = try! JSONEncoder().encode(movies) + func testDeleteOneDocument() throws { + let documents: Data = try JSONEncoder().encode(movies) let expectation = XCTestExpectation(description: "Delete one Movie") self.index.addDocuments( @@ -335,7 +322,7 @@ class DocumentsTests: XCTestCase { switch result { case .success(let task): XCTAssertEqual(Task.Status.succeeded, task.status) - XCTAssertEqual("documentDeletion", task.type) + XCTAssertEqual("documentDeletion", task.type.description) deleteExpectation.fulfill() case .failure: XCTFail("Failed to wait for task") @@ -351,8 +338,8 @@ class DocumentsTests: XCTestCase { self.wait(for: [deleteExpectation], timeout: 3.0) } - func testDeleteAllDocuments() { - let documents: Data = try! JSONEncoder().encode(movies) + func testDeleteAllDocuments() throws { + let documents: Data = try JSONEncoder().encode(movies) let expectation = XCTestExpectation(description: "Add documents") self.index.addDocuments( @@ -377,17 +364,13 @@ class DocumentsTests: XCTestCase { switch result { case .success(let task): XCTAssertEqual(Task.Status.succeeded, task.status) - XCTAssertEqual("documentDeletion", task.type) - if let details = task.details { - if let deletedDocuments = details.deletedDocuments { - XCTAssertGreaterThanOrEqual(deletedDocuments, 8) - } else { - XCTFail("deletedDocuments field should not be nil") - deleteExpectation.fulfill() - } + XCTAssertEqual("documentDeletion", task.type.description) + if case .documentDeletion(let details) = task.details { + // It's possible for this to number to be greater than 8 (the number of documents we have inserted) due + // to other integration tests populating the shared index. + XCTAssertGreaterThanOrEqual(details.deletedDocuments ?? -1, 8) } else { - XCTFail("deletedDocuments field should exists in details field of task") - deleteExpectation.fulfill() + XCTFail("documentDeletion details should be set by task") } deleteExpectation.fulfill() case .failure: @@ -405,8 +388,8 @@ class DocumentsTests: XCTestCase { self.wait(for: [deleteExpectation], timeout: TESTS_TIME_OUT) } - func testDeleteBatchDocuments() { - let documents: Data = try! JSONEncoder().encode(movies) + func testDeleteBatchDocuments() throws { + let documents: Data = try JSONEncoder().encode(movies) let expectation = XCTestExpectation(description: "Add documents") self.index.addDocuments( @@ -433,7 +416,7 @@ class DocumentsTests: XCTestCase { switch result { case .success(let task): XCTAssertEqual(Task.Status.succeeded, task.status) - XCTAssertEqual("documentDeletion", task.type) + XCTAssertEqual("documentDeletion", task.type.description) deleteExpectation.fulfill() case .failure: XCTFail("Failed to wait for task") @@ -450,8 +433,8 @@ class DocumentsTests: XCTestCase { } @available(*, deprecated, message: "Testing deprecated methods - marked deprecated to avoid additional warnings below.") - func testDeprecatedDeleteBatchDocuments() { - let documents: Data = try! JSONEncoder().encode(movies) + func testDeprecatedDeleteBatchDocuments() throws { + let documents: Data = try JSONEncoder().encode(movies) let expectation = XCTestExpectation(description: "Add documents") self.index.addDocuments( @@ -478,7 +461,7 @@ class DocumentsTests: XCTestCase { switch result { case .success(let task): XCTAssertEqual(Task.Status.succeeded, task.status) - XCTAssertEqual("documentDeletion", task.type) + XCTAssertEqual("documentDeletion", task.type.description) deleteExpectation.fulfill() case .failure: XCTFail("Failed to wait for task") @@ -495,4 +478,3 @@ class DocumentsTests: XCTestCase { } } // swiftlint:enable force_unwrapping -// swiftlint:enable force_try diff --git a/Tests/MeiliSearchIntegrationTests/DumpsTests.swift b/Tests/MeiliSearchIntegrationTests/DumpsTests.swift index bb5dbe60..fc5db922 100644 --- a/Tests/MeiliSearchIntegrationTests/DumpsTests.swift +++ b/Tests/MeiliSearchIntegrationTests/DumpsTests.swift @@ -5,18 +5,17 @@ import Foundation import FoundationNetworking #endif -// swiftlint:disable force_try class DumpsTests: XCTestCase { private var client: MeiliSearch! private var session: URLSessionProtocol! // MARK: Setup - override func setUp() { - super.setUp() + override func setUpWithError() throws { + try super.setUpWithError() if client == nil { session = URLSession(configuration: .ephemeral) - client = try! MeiliSearch(host: currentHost(), apiKey: "masterKey", session: session) + client = try MeiliSearch(host: currentHost(), apiKey: "masterKey", session: session) } } @@ -38,4 +37,3 @@ class DumpsTests: XCTestCase { self.wait(for: [expectation], timeout: TESTS_TIME_OUT) } } -// swiftlint:enable force_try diff --git a/Tests/MeiliSearchIntegrationTests/IndexesTests.swift b/Tests/MeiliSearchIntegrationTests/IndexesTests.swift index d3f3ea9d..1a35568a 100644 --- a/Tests/MeiliSearchIntegrationTests/IndexesTests.swift +++ b/Tests/MeiliSearchIntegrationTests/IndexesTests.swift @@ -5,19 +5,18 @@ import Foundation import FoundationNetworking #endif -// swiftlint:disable force_try class IndexesTests: XCTestCase { private var client: MeiliSearch! private var session: URLSessionProtocol! private let uid: String = "books_test" private var index: Indexes! - override func setUp() { - super.setUp() + override func setUpWithError() throws { + try super.setUpWithError() if client == nil { session = URLSession(configuration: .ephemeral) - client = try! MeiliSearch(host: currentHost(), apiKey: "masterKey", session: session) + client = try MeiliSearch(host: currentHost(), apiKey: "masterKey", session: session) } index = self.client.index(self.uid) @@ -57,7 +56,7 @@ class IndexesTests: XCTestCase { self.client.waitForTask(task: task, options: WaitOptions(timeOut: 10.0)) { result in switch result { case .success(let task): - XCTAssertEqual("indexCreation", task.type) + XCTAssertEqual("indexCreation", task.type.description) XCTAssertEqual(task.status, Task.Status.succeeded) createExpectation.fulfill() case .failure(let error): @@ -93,7 +92,7 @@ class IndexesTests: XCTestCase { createGenericIndex(client: self.client, uid: self.uid ) { result in switch result { case .success(let task): - XCTAssertEqual("indexCreation", task.type) + XCTAssertEqual("indexCreation", task.type.description) XCTAssertEqual(task.status, Task.Status.succeeded) createExpectation.fulfill() case .failure(let error): @@ -111,7 +110,7 @@ class IndexesTests: XCTestCase { self.client.waitForTask(task: task) { result in switch result { case .success(let task): - XCTAssertEqual("indexCreation", task.type) + XCTAssertEqual("indexCreation", task.type.description) XCTAssertEqual(task.status, Task.Status.failed) if let error = task.error { XCTAssertEqual(error.code, "index_already_exists") @@ -221,14 +220,10 @@ class IndexesTests: XCTestCase { self.client.waitForTask(task: task) { result in switch result { case .success(let task): - XCTAssertEqual("indexUpdate", task.type) + XCTAssertEqual("indexUpdate", task.type.description) XCTAssertEqual(task.status, Task.Status.succeeded) - if let details = task.details { - if let primaryKey = details.primaryKey { - XCTAssertEqual("random", primaryKey) - } else { - XCTFail("Primary key should not be nil") - } + if case .indexUpdate(let details) = task.details, let primaryKey = details.primaryKey { + XCTAssertEqual("random", primaryKey) } else { XCTFail("Primary key should exists in details field of task") } @@ -267,7 +262,7 @@ class IndexesTests: XCTestCase { deleteIndex(client: self.client, uid: self.uid) { result in switch result { case .success(let task): - XCTAssertEqual("indexDeletion", task.type) + XCTAssertEqual("indexDeletion", task.type.description) XCTAssertEqual(task.status, Task.Status.succeeded) deleteException.fulfill() case .failure(let error): @@ -279,4 +274,3 @@ class IndexesTests: XCTestCase { self.wait(for: [deleteException], timeout: TESTS_TIME_OUT) } } -// swiftlint:enable force_try diff --git a/Tests/MeiliSearchIntegrationTests/KeysTests.swift b/Tests/MeiliSearchIntegrationTests/KeysTests.swift index f0342e54..c2e301b7 100644 --- a/Tests/MeiliSearchIntegrationTests/KeysTests.swift +++ b/Tests/MeiliSearchIntegrationTests/KeysTests.swift @@ -5,18 +5,17 @@ import Foundation import FoundationNetworking #endif -// swiftlint:disable force_try class KeysTests: XCTestCase { private var client: MeiliSearch! private var session: URLSessionProtocol! // MARK: Setup - override func setUp() { - super.setUp() + override func setUpWithError() throws { + try super.setUpWithError() session = URLSession(configuration: .ephemeral) - client = try! MeiliSearch(host: currentHost(), apiKey: "masterKey", session: session) + client = try MeiliSearch(host: currentHost(), apiKey: "masterKey", session: session) let semaphore = XCTestExpectation(description: "Setup: delete all keys") diff --git a/Tests/MeiliSearchIntegrationTests/SearchTests.swift b/Tests/MeiliSearchIntegrationTests/SearchTests.swift index 60d84972..45af94eb 100644 --- a/Tests/MeiliSearchIntegrationTests/SearchTests.swift +++ b/Tests/MeiliSearchIntegrationTests/SearchTests.swift @@ -18,7 +18,7 @@ private let books: [Book] = [ ] // swiftlint:disable force_unwrapping -// swiftlint:disable force_try + private let nestedBooks: [NestedBook] = [ NestedBook(id: 123, title: "Pride and Prejudice", info: InfoNested(comment: "A great book", reviewNb: 100), genres: ["Classic Regency nove"]), NestedBook(id: 456, title: "Le Petit Prince", info: InfoNested(comment: "A french book", reviewNb: 100), genres: ["Novel"]), @@ -35,11 +35,11 @@ class SearchTests: XCTestCase { // MARK: Setup - override func setUp() { - super.setUp() + override func setUpWithError() throws { + try super.setUpWithError() session = URLSession(configuration: .ephemeral) - client = try! MeiliSearch(host: currentHost(), apiKey: "masterKey", session: session) + client = try MeiliSearch(host: currentHost(), apiKey: "masterKey", session: session) index = self.client.index(self.uid) nestedIndex = self.client.index(self.nested_uid) @@ -138,6 +138,98 @@ class SearchTests: XCTestCase { self.wait(for: [expectation], timeout: TESTS_TIME_OUT) } + // MARK: Search ranking + func testSearchRankingScore() { + let expectation = XCTestExpectation(description: "Search for Books with query") + + typealias MeiliResult = Result, Swift.Error> + let query = "Moreninha" + + self.index.search(SearchParameters(query: query, showRankingScore: true)) { (result: MeiliResult) in + switch result { + case .success(let response): + let result = response as! SearchResult + XCTAssertEqual(result.hits.count, 1) + XCTAssertGreaterThan(result.hits[0].rankingScore ?? 0, 0.1) + expectation.fulfill() + case .failure(let error): + dump(error) + XCTFail("Failed to search with testSearchRankingScore") + expectation.fulfill() + } + } + + self.wait(for: [expectation], timeout: TESTS_TIME_OUT) + } + + func testSearchBoxEncodingWithScore() { + let expectation = XCTestExpectation(description: "Search for Books with query") + + let expectedValue = """ + {"hits":[{"_rankingScore":0.5,"comment":"A Book from Joaquim Manuel de Macedo","genres":["Novel"],"id":1844,"title":"A Moreninha"}],"processingTimeMs":0,"query":"Moreninha"} + """ + + typealias MeiliResult = Result, Swift.Error> + let query = "Moreninha" + + self.index.search(SearchParameters(query: query, showRankingScore: true)) { (result: MeiliResult) in + switch result { + case .success(let response): + do { + // the ranking score and time can change for many reasons, of which is not relevant here. we set it to a constant to test the encoding. + response.processingTimeMs = 0 + response.hits[0].rankingScore = 0.5 + let encoder = JSONEncoder() + encoder.outputFormatting = .sortedKeys + let data = try encoder.encode(response) + XCTAssertEqual(String(decoding: data, as: UTF8.self), expectedValue) + } catch { + XCTFail("Failed to encode search result") + } + expectation.fulfill() + case .failure(let error): + dump(error) + XCTFail("Failed to search with testSearchBoxEncodingWithScore") + expectation.fulfill() + } + } + + self.wait(for: [expectation], timeout: TESTS_TIME_OUT) + } + + func testSearchBoxEncodingWithoutScore() { + let expectation = XCTestExpectation(description: "Search for Books with query") + + let expectedValue = """ + {"hits":[{"comment":"A Book from Joaquim Manuel de Macedo","genres":["Novel"],"id":1844,"title":"A Moreninha"}],"processingTimeMs":0,"query":"Moreninha"} + """ + + typealias MeiliResult = Result, Swift.Error> + let query = "Moreninha" + + self.index.search(SearchParameters(query: query, showRankingScore: false)) { (result: MeiliResult) in + switch result { + case .success(let response): + do { + let encoder = JSONEncoder() + encoder.outputFormatting = .sortedKeys + response.processingTimeMs = 0 + let data = try encoder.encode(response) + XCTAssertEqual(String(decoding: data, as: UTF8.self), expectedValue) + } catch { + XCTFail("Failed to encode search result") + } + expectation.fulfill() + case .failure(let error): + dump(error) + XCTFail("Failed to search with testSearchBoxEncodingWithoutScore") + expectation.fulfill() + } + } + + self.wait(for: [expectation], timeout: TESTS_TIME_OUT) + } + // MARK: Basic search with finite pagination func testBasicSearchWithFinitePagination() { let expectation = XCTestExpectation(description: "Search for Books with finite pagination") @@ -466,7 +558,7 @@ class SearchTests: XCTestCase { XCTAssertEqual(result.limit, limit) XCTAssertEqual(documents.hits.count, 1) - let book: Book = documents.hits[0] + let book: SearchHit = documents.hits[0] XCTAssertEqual("…from Joaquim Manuel de Macedo", book.formatted!.comment!) expectation.fulfill() case .failure(let error): @@ -494,7 +586,7 @@ class SearchTests: XCTestCase { self.index.search(searchParameters) { (result: MeiliResult) in switch result { case .success(let documents): - let book: Book = documents.hits[0] + let book: SearchHit = documents.hits[0] XCTAssertEqual("(ꈍᴗꈍ)Joaquim Manuel(ꈍᴗꈍ)", book.formatted!.comment!) expectation.fulfill() case .failure(let error): @@ -527,7 +619,7 @@ class SearchTests: XCTestCase { XCTAssertEqual(result.limit, limit) XCTAssertEqual(documents.hits.count, 2) - let moreninhaBook: Book = documents.hits.first(where: { book in book.id == 1844 })! + let moreninhaBook: SearchHit = documents.hits.first(where: { book in book.id == 1844 })! XCTAssertEqual("A Book from Joaquim Manuel…", moreninhaBook.formatted!.comment!) expectation.fulfill() case .failure(let error): @@ -878,7 +970,7 @@ class SearchTests: XCTestCase { XCTAssertEqual(documents.query, query) XCTAssertEqual(result.limit, limit) XCTAssertEqual(documents.hits.count, 1) - guard let book: Book = documents.hits.first(where: { book in book.id == 1344 }) else { + guard let book: SearchHit = documents.hits.first(where: { book in book.id == 1344 }) else { XCTFail("Failed to search with testSearchFilterWithEmptySpace") expectation.fulfill() return @@ -1040,4 +1132,3 @@ class SearchTests: XCTestCase { } } // swiftlint:enable force_unwrapping -// swiftlint:enable force_try diff --git a/Tests/MeiliSearchIntegrationTests/SettingsTests.swift b/Tests/MeiliSearchIntegrationTests/SettingsTests.swift index d28d51a0..00d749cb 100644 --- a/Tests/MeiliSearchIntegrationTests/SettingsTests.swift +++ b/Tests/MeiliSearchIntegrationTests/SettingsTests.swift @@ -5,7 +5,6 @@ import Foundation import FoundationNetworking #endif -// swiftlint:disable force_try class SettingsTests: XCTestCase { private var client: MeiliSearch! private var index: Indexes! @@ -31,16 +30,28 @@ class SettingsTests: XCTestCase { private let defaultStopWords: [String] = [] private let defaultSynonyms: [String: [String]] = [:] private let defaultPagination: Pagination = .init(maxTotalHits: 1000) + private let defaultTypoTolerance: TypoTolerance = .init( + enabled: true, + minWordSizeForTypos: .init(oneTypo: 5, twoTypos: 9), + disableOnWords: [], + disableOnAttributes: [] + ) + private let defaultTypoToleranceResult: TypoToleranceResult = .init( + enabled: true, + minWordSizeForTypos: .init(oneTypo: 5, twoTypos: 9), + disableOnWords: [], + disableOnAttributes: [] + ) private var defaultGlobalSettings: Setting? private var defaultGlobalReturnedSettings: SettingResult? // MARK: Setup - override func setUp() { - super.setUp() + override func setUpWithError() throws { + try super.setUpWithError() session = URLSession(configuration: .ephemeral) - client = try! MeiliSearch(host: currentHost(), apiKey: "masterKey", session: session) + client = try MeiliSearch(host: currentHost(), apiKey: "masterKey", session: session) index = self.client.index(self.uid) let createExpectation = XCTestExpectation(description: "Create Movies index") @@ -68,7 +79,8 @@ class SettingsTests: XCTestCase { separatorTokens: self.defaultSeparatorTokens, nonSeparatorTokens: self.defaultNonSeparatorTokens, dictionary: self.defaultDictionary, - pagination: self.defaultPagination + pagination: self.defaultPagination, + typoTolerance: self.defaultTypoTolerance ) self.defaultGlobalReturnedSettings = SettingResult( @@ -83,7 +95,8 @@ class SettingsTests: XCTestCase { separatorTokens: self.defaultSeparatorTokens, nonSeparatorTokens: self.defaultNonSeparatorTokens, dictionary: self.defaultDictionary, - pagination: self.defaultPagination + pagination: self.defaultPagination, + typoTolerance: self.defaultTypoToleranceResult ) } @@ -118,16 +131,12 @@ class SettingsTests: XCTestCase { self.client.waitForTask(task: task) { result in switch result { case .success(let task): - XCTAssertEqual("settingsUpdate", task.type) + XCTAssertEqual("settingsUpdate", task.type.description) XCTAssertEqual(Task.Status.succeeded, task.status) - if let details = task.details { - if let filterableAttributes = details.filterableAttributes { - XCTAssertEqual(newFilterableAttributes, filterableAttributes) - } else { - XCTFail("filterableAttributes should not be nil") - } + if case .settingsUpdate(let details) = task.details { + XCTAssertEqual(newFilterableAttributes, details.filterableAttributes) } else { - XCTFail("details should exists in details field of task") + XCTFail("settingsUpdate details should be set by task") } expectation.fulfill() case .failure(let error): @@ -155,7 +164,7 @@ class SettingsTests: XCTestCase { self.client.waitForTask(task: task) { result in switch result { case .success(let task): - XCTAssertEqual("settingsUpdate", task.type) + XCTAssertEqual("settingsUpdate", task.type.description) XCTAssertEqual(Task.Status.succeeded, task.status) expectation.fulfill() case .failure(let error): @@ -204,16 +213,12 @@ class SettingsTests: XCTestCase { self.client.waitForTask(task: task) { result in switch result { case .success(let task): - XCTAssertEqual("settingsUpdate", task.type) + XCTAssertEqual("settingsUpdate", task.type.description) XCTAssertEqual(Task.Status.succeeded, task.status) - if let details = task.details { - if let displayedAttributes = details.displayedAttributes { - XCTAssertEqual(newDisplayedAttributes, displayedAttributes) - } else { - XCTFail("displayedAttributes should not be nil") - } + if case .settingsUpdate(let details) = task.details { + XCTAssertEqual(newDisplayedAttributes, details.displayedAttributes) } else { - XCTFail("details should exists in details field of task") + XCTFail("settingsUpdate details should be set by task") } expectation.fulfill() case .failure(let error): @@ -241,7 +246,7 @@ class SettingsTests: XCTestCase { self.client.waitForTask(task: task) { result in switch result { case .success(let task): - XCTAssertEqual("settingsUpdate", task.type) + XCTAssertEqual("settingsUpdate", task.type.description) XCTAssertEqual(Task.Status.succeeded, task.status) expectation.fulfill() case .failure(let error): @@ -289,16 +294,12 @@ class SettingsTests: XCTestCase { self.client.waitForTask(task: task) { result in switch result { case .success(let task): - XCTAssertEqual("settingsUpdate", task.type) + XCTAssertEqual("settingsUpdate", task.type.description) XCTAssertEqual(Task.Status.succeeded, task.status) - if let details = task.details { - if let distinctAttribute = details.distinctAttribute { - XCTAssertEqual(newDistinctAttribute, distinctAttribute) - } else { - XCTFail("distinctAttribute should not be nil") - } + if case .settingsUpdate(let details) = task.details { + XCTAssertEqual(newDistinctAttribute, details.distinctAttribute) } else { - XCTFail("details should exists in details field of task") + XCTFail("settingsUpdate details should be set by task") } expectation.fulfill() case .failure(let error): @@ -325,7 +326,7 @@ class SettingsTests: XCTestCase { self.client.waitForTask(task: task) { result in switch result { case .success(let task): - XCTAssertEqual("settingsUpdate", task.type) + XCTAssertEqual("settingsUpdate", task.type.description) XCTAssertEqual(Task.Status.succeeded, task.status) expectation.fulfill() case .failure(let error): @@ -379,16 +380,12 @@ class SettingsTests: XCTestCase { self.client.waitForTask(task: task) { result in switch result { case .success(let task): - XCTAssertEqual("settingsUpdate", task.type) + XCTAssertEqual("settingsUpdate", task.type.description) XCTAssertEqual(Task.Status.succeeded, task.status) - if let details = task.details { - if let rankingRules = details.rankingRules { - XCTAssertEqual(newRankingRules, rankingRules) - } else { - XCTFail("rankingRules should not be nil") - } + if case .settingsUpdate(let details) = task.details { + XCTAssertEqual(newRankingRules, details.rankingRules) } else { - XCTFail("details should exists in details field of task") + XCTFail("settingsUpdate details should be set by task") } expectation.fulfill() case .failure(let error): @@ -416,7 +413,7 @@ class SettingsTests: XCTestCase { self.client.waitForTask(task: task) { result in switch result { case .success(let task): - XCTAssertEqual("settingsUpdate", task.type) + XCTAssertEqual("settingsUpdate", task.type.description) XCTAssertEqual(Task.Status.succeeded, task.status) expectation.fulfill() case .failure(let error): @@ -469,16 +466,12 @@ class SettingsTests: XCTestCase { self.client.waitForTask(task: task) { result in switch result { case .success(let task): - XCTAssertEqual("settingsUpdate", task.type) + XCTAssertEqual("settingsUpdate", task.type.description) XCTAssertEqual(Task.Status.succeeded, task.status) - if let details = task.details { - if let searchableAttributes = details.searchableAttributes { - XCTAssertEqual(newSearchableAttributes, searchableAttributes) - } else { - XCTFail("searchableAttributes should not be nil") - } + if case .settingsUpdate(let details) = task.details { + XCTAssertEqual(newSearchableAttributes, details.searchableAttributes) } else { - XCTFail("details should exists in details field of task") + XCTFail("settingsUpdate details should be set by task") } expectation.fulfill() case .failure(let error): @@ -506,7 +499,7 @@ class SettingsTests: XCTestCase { self.client.waitForTask(task: task) { result in switch result { case .success(let task): - XCTAssertEqual("settingsUpdate", task.type) + XCTAssertEqual("settingsUpdate", task.type.description) XCTAssertEqual(Task.Status.succeeded, task.status) expectation.fulfill() case .failure(let error): @@ -525,6 +518,96 @@ class SettingsTests: XCTestCase { self.wait(for: [expectation], timeout: TESTS_TIME_OUT) } + // MARK: Typo Tolerance + + func testGetTypoTolerance() { + let expectation = XCTestExpectation(description: "Get current typo tolerance") + + self.index.getTypoTolerance { result in + switch result { + case .success(let typoTolerance): + XCTAssertEqual(self.defaultTypoToleranceResult, typoTolerance) + expectation.fulfill() + case .failure(let error): + dump(error) + XCTFail("Failed to get typo tolerance") + expectation.fulfill() + } + } + + self.wait(for: [expectation], timeout: TESTS_TIME_OUT) + } + + func testUpdateTypoTolerance() { + let expectation = XCTestExpectation(description: "Update settings for typo tolerance") + + let newTypoTolerance: TypoTolerance = .init( + minWordSizeForTypos: .init( + oneTypo: 1, + twoTypos: 2 + ), + disableOnWords: ["to"], + disableOnAttributes: ["genre"] + ) + + self.index.updateTypoTolerance(newTypoTolerance) { result in + switch result { + case .success(let task): + self.client.waitForTask(task: task) { result in + switch result { + case .success(let task): + XCTAssertEqual("settingsUpdate", task.type.description) + XCTAssertEqual(Task.Status.succeeded, task.status) + if case .settingsUpdate(let details) = task.details { + XCTAssertEqual(newTypoTolerance, details.typoTolerance) + } else { + XCTFail("settingsUpdate details should be set by task") + } + expectation.fulfill() + case .failure(let error): + dump(error) + XCTFail("Failed to wait for task") + expectation.fulfill() + } + } + case .failure(let error): + dump(error) + XCTFail("Failed updating typo tolerance") + expectation.fulfill() + } + } + + self.wait(for: [expectation], timeout: TESTS_TIME_OUT) + } + + func testResetTypoTolerance() { + let expectation = XCTestExpectation(description: "Reset settings for typo tolerance") + + self.index.resetTypoTolerance { result in + switch result { + case .success(let task): + self.client.waitForTask(task: task) { result in + switch result { + case .success(let task): + XCTAssertEqual("settingsUpdate", task.type.description) + XCTAssertEqual(Task.Status.succeeded, task.status) + expectation.fulfill() + case .failure(let error): + dump(error) + XCTFail("Failed to wait for task") + expectation.fulfill() + } + } + case .failure(let error): + dump(error) + XCTFail("Failed reseting typo tolerance") + expectation.fulfill() + } + } + + self.wait(for: [expectation], timeout: TESTS_TIME_OUT) + } + // MARK: Separator Tokens func testGetSeparatorTokens() { @@ -560,16 +643,12 @@ class SettingsTests: XCTestCase { self.client.waitForTask(task: task) { result in switch result { case .success(let task): - XCTAssertEqual("settingsUpdate", task.type) + XCTAssertEqual("settingsUpdate", task.type.description) XCTAssertEqual(Task.Status.succeeded, task.status) - if let details = task.details { - if let separatorTokens = details.separatorTokens { - XCTAssertEqual(newSeparatorTokens, separatorTokens) - } else { - XCTFail("separatorTokens should not be nil") - } + if case .settingsUpdate(let details) = task.details { + XCTAssertEqual(newSeparatorTokens, details.separatorTokens) } else { - XCTFail("details should exists in details field of task") + XCTFail("settingsUpdate details should be set by task") } expectation.fulfill() case .failure(let error): @@ -597,7 +676,7 @@ class SettingsTests: XCTestCase { self.client.waitForTask(task: task) { result in switch result { case .success(let task): - XCTAssertEqual("settingsUpdate", task.type) + XCTAssertEqual("settingsUpdate", task.type.description) XCTAssertEqual(Task.Status.succeeded, task.status) expectation.fulfill() case .failure(let error): @@ -651,16 +730,12 @@ class SettingsTests: XCTestCase { self.client.waitForTask(task: task) { result in switch result { case .success(let task): - XCTAssertEqual("settingsUpdate", task.type) + XCTAssertEqual("settingsUpdate", task.type.description) XCTAssertEqual(Task.Status.succeeded, task.status) - if let details = task.details { - if let nonSeparatorTokens = details.nonSeparatorTokens { - XCTAssertEqual(newNonSeparatorTokens, nonSeparatorTokens) - } else { - XCTFail("nonSeparatorTokens should not be nil") - } + if case .settingsUpdate(let details) = task.details { + XCTAssertEqual(newNonSeparatorTokens, details.nonSeparatorTokens) } else { - XCTFail("details should exists in details field of task") + XCTFail("settingsUpdate details should be set by task") } expectation.fulfill() case .failure(let error): @@ -688,7 +763,7 @@ class SettingsTests: XCTestCase { self.client.waitForTask(task: task) { result in switch result { case .success(let task): - XCTAssertEqual("settingsUpdate", task.type) + XCTAssertEqual("settingsUpdate", task.type.description) XCTAssertEqual(Task.Status.succeeded, task.status) expectation.fulfill() case .failure(let error): @@ -742,16 +817,12 @@ class SettingsTests: XCTestCase { self.client.waitForTask(task: task) { result in switch result { case .success(let task): - XCTAssertEqual("settingsUpdate", task.type) + XCTAssertEqual("settingsUpdate", task.type.description) XCTAssertEqual(Task.Status.succeeded, task.status) - if let details = task.details { - if let dictionary = details.dictionary { - XCTAssertEqual(newDictionary.sorted(), dictionary.sorted()) - } else { - XCTFail("dictionary should not be nil") - } + if case .settingsUpdate(let details) = task.details { + XCTAssertEqual(newDictionary.sorted(), details.dictionary?.sorted()) } else { - XCTFail("details should exists in details field of task") + XCTFail("settingsUpdate details should be set by task") } expectation.fulfill() case .failure(let error): @@ -779,7 +850,7 @@ class SettingsTests: XCTestCase { self.client.waitForTask(task: task) { result in switch result { case .success(let task): - XCTAssertEqual("settingsUpdate", task.type) + XCTAssertEqual("settingsUpdate", task.type.description) XCTAssertEqual(Task.Status.succeeded, task.status) expectation.fulfill() case .failure(let error): @@ -829,16 +900,12 @@ class SettingsTests: XCTestCase { self.client.waitForTask(task: task) { result in switch result { case .success(let task): - XCTAssertEqual("settingsUpdate", task.type) + XCTAssertEqual("settingsUpdate", task.type.description) XCTAssertEqual(Task.Status.succeeded, task.status) - if let details = task.details { - if let pagination = details.pagination { - XCTAssertEqual(newPaginationSettings, pagination) - } else { - XCTFail("pagination should not be nil") - } + if case .settingsUpdate(let details) = task.details { + XCTAssertEqual(newPaginationSettings, details.pagination) } else { - XCTFail("details should exists in details field of task") + XCTFail("settingsUpdate details should be set by task") } expectation.fulfill() case .failure(let error): @@ -866,7 +933,7 @@ class SettingsTests: XCTestCase { self.client.waitForTask(task: task) { result in switch result { case .success(let task): - XCTAssertEqual("settingsUpdate", task.type) + XCTAssertEqual("settingsUpdate", task.type.description) XCTAssertEqual(Task.Status.succeeded, task.status) expectation.fulfill() case .failure(let error): @@ -916,16 +983,12 @@ class SettingsTests: XCTestCase { self.client.waitForTask(task: task) { result in switch result { case .success(let task): - XCTAssertEqual("settingsUpdate", task.type) + XCTAssertEqual("settingsUpdate", task.type.description) XCTAssertEqual(Task.Status.succeeded, task.status) - if let details = task.details { - if let stopWords = details.stopWords { - XCTAssertEqual(newStopWords, stopWords) - } else { - XCTFail("stopWords should not be nil") - } + if case .settingsUpdate(let details) = task.details { + XCTAssertEqual(newStopWords, details.stopWords) } else { - XCTFail("details should exists in details field of task") + XCTFail("settingsUpdate details should be set by task") } expectation.fulfill() case .failure(let error): @@ -954,16 +1017,12 @@ class SettingsTests: XCTestCase { self.client.waitForTask(task: task) { result in switch result { case .success(let task): - XCTAssertEqual("settingsUpdate", task.type) + XCTAssertEqual("settingsUpdate", task.type.description) XCTAssertEqual(Task.Status.succeeded, task.status) - if let details = task.details { - if let stopWords = details.stopWords { - XCTAssertEqual(emptyStopWords, stopWords) - } else { - XCTFail("stopWords should be nil") - } + if case .settingsUpdate(let details) = task.details { + XCTAssertEqual(emptyStopWords, details.stopWords) } else { - XCTFail("details should exists in details field of task") + XCTFail("settingsUpdate details should be set by task") } expectation.fulfill() case .failure(let error): @@ -992,16 +1051,12 @@ class SettingsTests: XCTestCase { self.client.waitForTask(task: task) { result in switch result { case .success(let task): - XCTAssertEqual("settingsUpdate", task.type) + XCTAssertEqual("settingsUpdate", task.type.description) XCTAssertEqual(Task.Status.succeeded, task.status) - if let details = task.details { - if details.stopWords == nil { - XCTAssertEqual(nilStopWords, details.stopWords) - } else { - XCTFail("stopWords should be nil") - } + if case .settingsUpdate(let details) = task.details { + XCTAssertEqual(nilStopWords, details.stopWords) } else { - XCTFail("details should exists in details field of task") + XCTFail("settingsUpdate details should be set by task") } expectation.fulfill() case .failure(let error): @@ -1029,7 +1084,7 @@ class SettingsTests: XCTestCase { self.client.waitForTask(task: task) { result in switch result { case .success(let task): - XCTAssertEqual("settingsUpdate", task.type) + XCTAssertEqual("settingsUpdate", task.type.description) XCTAssertEqual(Task.Status.succeeded, task.status) expectation.fulfill() case .failure(let error): @@ -1084,16 +1139,12 @@ class SettingsTests: XCTestCase { self.client.waitForTask(task: task) { result in switch result { case .success(let task): - XCTAssertEqual("settingsUpdate", task.type) + XCTAssertEqual("settingsUpdate", task.type.description) XCTAssertEqual(Task.Status.succeeded, task.status) - if let details = task.details { - if let synonyms = details.synonyms { - XCTAssertEqual(newSynonyms, synonyms) - } else { - XCTFail("synonyms should not be nil") - } + if case .settingsUpdate(let details) = task.details { + XCTAssertEqual(newSynonyms, details.synonyms) } else { - XCTFail("details should exists in details field of task") + XCTFail("settingsUpdate details should be set by task") } expectation.fulfill() case .failure(let error): @@ -1122,16 +1173,12 @@ class SettingsTests: XCTestCase { self.client.waitForTask(task: task) { result in switch result { case .success(let task): - XCTAssertEqual("settingsUpdate", task.type) + XCTAssertEqual("settingsUpdate", task.type.description) XCTAssertEqual(Task.Status.succeeded, task.status) - if let details = task.details { - if let synonyms = details.synonyms { - XCTAssertEqual(newSynonyms, synonyms) - } else { - XCTFail("synonyms should not be nil") - } + if case .settingsUpdate(let details) = task.details { + XCTAssertEqual(newSynonyms, details.synonyms) } else { - XCTFail("details should exists in details field of task") + XCTFail("settingsUpdate details should be set by task") } expectation.fulfill() case .failure(let error): @@ -1161,16 +1208,12 @@ class SettingsTests: XCTestCase { self.client.waitForTask(task: task) { result in switch result { case .success(let task): - XCTAssertEqual("settingsUpdate", task.type) + XCTAssertEqual("settingsUpdate", task.type.description) XCTAssertEqual(Task.Status.succeeded, task.status) - if let details = task.details { - if details.synonyms == nil { - XCTAssertEqual(newSynonyms, details.synonyms) - } else { - XCTFail("synonyms should not be nil") - } + if case .settingsUpdate(let details) = task.details { + XCTAssertEqual(newSynonyms, details.synonyms) } else { - XCTFail("details should exists in details field of task") + XCTFail("settingsUpdate details should be set by task") } expectation.fulfill() case .failure(let error): @@ -1198,7 +1241,7 @@ class SettingsTests: XCTestCase { self.client.waitForTask(task: task) { result in switch result { case .success(let task): - XCTAssertEqual("settingsUpdate", task.type) + XCTAssertEqual("settingsUpdate", task.type.description) XCTAssertEqual(Task.Status.succeeded, task.status) expectation.fulfill() case .failure(let error): @@ -1260,7 +1303,8 @@ class SettingsTests: XCTestCase { separatorTokens: [], nonSeparatorTokens: [], dictionary: [], - pagination: .init(maxTotalHits: 1000) + pagination: .init(maxTotalHits: 1000), + typoTolerance: defaultTypoToleranceResult ) let expectation = XCTestExpectation(description: "Update settings") @@ -1270,14 +1314,14 @@ class SettingsTests: XCTestCase { self.client.waitForTask(task: task) { result in switch result { case .success(let task): - XCTAssertEqual("settingsUpdate", task.type) + XCTAssertEqual("settingsUpdate", task.type.description) XCTAssertEqual(Task.Status.succeeded, task.status) - if let details = task.details { + if case .settingsUpdate(let details) = task.details { XCTAssertEqual(expectedSettingResult.rankingRules, details.rankingRules) XCTAssertEqual(expectedSettingResult.searchableAttributes, details.searchableAttributes) XCTAssertEqual(expectedSettingResult.stopWords, details.stopWords) } else { - XCTFail("details should exists in details field of task") + XCTFail("settingsUpdate details should be set by task") } expectation.fulfill() case .failure(let error): @@ -1303,12 +1347,12 @@ class SettingsTests: XCTestCase { self.client.waitForTask(task: task) { result in switch result { case .success(let task): - XCTAssertEqual("settingsUpdate", task.type) + XCTAssertEqual("settingsUpdate", task.type.description) XCTAssertEqual(Task.Status.succeeded, task.status) - if let details = task.details { + if case .settingsUpdate(let details) = task.details { XCTAssertEqual(expectedSettingResult.rankingRules, details.rankingRules) } else { - XCTFail("details should exists in details field of task") + XCTFail("settingsUpdate details should be set by task") } overrideSettingsExpectation.fulfill() case .failure(let error): @@ -1341,7 +1385,8 @@ class SettingsTests: XCTestCase { separatorTokens: ["&"], nonSeparatorTokens: ["#"], dictionary: ["J.K"], - pagination: .init(maxTotalHits: 500) + pagination: .init(maxTotalHits: 500), + typoTolerance: nil ) let expectedSettingResult = SettingResult( @@ -1356,7 +1401,8 @@ class SettingsTests: XCTestCase { separatorTokens: ["&"], nonSeparatorTokens: ["#"], dictionary: ["J.K"], - pagination: .init(maxTotalHits: 500) + pagination: .init(maxTotalHits: 500), + typoTolerance: defaultTypoToleranceResult ) self.index.updateSettings(newSettings) { result in @@ -1365,9 +1411,9 @@ class SettingsTests: XCTestCase { self.client.waitForTask(task: task) { result in switch result { case .success(let task): - XCTAssertEqual("settingsUpdate", task.type) + XCTAssertEqual("settingsUpdate", task.type.description) XCTAssertEqual(Task.Status.succeeded, task.status) - if let details = task.details { + if case .settingsUpdate(let details) = task.details { XCTAssertEqual(expectedSettingResult.rankingRules, details.rankingRules) XCTAssertEqual(expectedSettingResult.searchableAttributes, details.searchableAttributes) XCTAssertEqual(expectedSettingResult.displayedAttributes, details.displayedAttributes) @@ -1379,7 +1425,7 @@ class SettingsTests: XCTestCase { XCTAssertEqual(expectedSettingResult.dictionary, details.dictionary) XCTAssertEqual(expectedSettingResult.pagination.maxTotalHits, details.pagination?.maxTotalHits) } else { - XCTFail("details should exists in details field of task") + XCTFail("settingsUpdate details should be set by task") } expectation.fulfill() case .failure(let error): @@ -1407,7 +1453,7 @@ class SettingsTests: XCTestCase { self.client.waitForTask(task: task) { result in switch result { case .success(let task): - XCTAssertEqual("settingsUpdate", task.type) + XCTAssertEqual("settingsUpdate", task.type.description) XCTAssertEqual(Task.Status.succeeded, task.status) expectation.fulfill() case .failure(let error): @@ -1426,4 +1472,3 @@ class SettingsTests: XCTestCase { self.wait(for: [expectation], timeout: TESTS_TIME_OUT) } } -// swiftlint:enable force_try diff --git a/Tests/MeiliSearchIntegrationTests/TaskTests.swift b/Tests/MeiliSearchIntegrationTests/TaskTests.swift index e108a145..75bc4716 100644 --- a/Tests/MeiliSearchIntegrationTests/TaskTests.swift +++ b/Tests/MeiliSearchIntegrationTests/TaskTests.swift @@ -5,7 +5,6 @@ import Foundation import FoundationNetworking #endif -// swiftlint:disable force_try class TasksTests: XCTestCase { private var client: MeiliSearch! private var index: Indexes! @@ -14,10 +13,10 @@ class TasksTests: XCTestCase { // MARK: Setup - override func setUp() { - super.setUp() + override func setUpWithError() throws { + try super.setUpWithError() session = URLSession(configuration: .ephemeral) - client = try! MeiliSearch(host: currentHost(), apiKey: "masterKey", session: session) + client = try MeiliSearch(host: currentHost(), apiKey: "masterKey", session: session) index = self.client.index(self.uid) let createExpectation = XCTestExpectation(description: "Create Movies index") createGenericIndex(client: self.client, uid: self.uid) { result in @@ -43,7 +42,7 @@ class TasksTests: XCTestCase { self.index.getTask(taskUid: task.taskUid) { result in switch result { case .success(let task): - XCTAssertEqual(task.type, "documentAdditionOrUpdate") + XCTAssertEqual(task.type.description, "documentAdditionOrUpdate") addDocExpectation.fulfill() case .failure(let error): dump(error) @@ -75,6 +74,9 @@ class TasksTests: XCTestCase { case .success(let tasks): // Only one because index has been deleted and recreated XCTAssertEqual(tasks.results.count, 1) + XCTAssertEqual(tasks.total, 1) + XCTAssertNotNil(tasks.results[0].startedAt) + XCTAssertNotNil(tasks.results[0].finishedAt) expectation.fulfill() case .failure(let error): dump(error) @@ -105,7 +107,7 @@ class TasksTests: XCTestCase { self.client.getTask(taskUid: task.taskUid) { result in switch result { case .success(let task): - XCTAssertEqual(task.type, "indexCreation") + XCTAssertEqual(task.type.description, "indexCreation") addDocExpectation.fulfill() case .failure(let error): dump(error) @@ -152,6 +154,69 @@ class TasksTests: XCTestCase { self.wait(for: [expectation], timeout: TESTS_TIME_OUT) } + func testCancelTask() { + let addDocExpectation = XCTestExpectation(description: "Add documents") + addDocuments(client: self.client, uid: self.uid, primaryKey: nil) { result in + switch result { + case .success(let task): + self.client.cancelTasks(filter: .init(uids: [task.uid])) { _ in + self.client.getTasks { result in + switch result { + case .success(let tasks): + XCTAssertGreaterThanOrEqual(tasks.results.count, 2) + XCTAssertEqual(tasks.results[0].type, .taskCancelation) + XCTAssertEqual(tasks.results[1].type, .documentAdditionOrUpdate) + case .failure(let error): + dump(error) + XCTFail("Failed to get tasks") + } + addDocExpectation.fulfill() + } + } + + case .failure: + XCTFail("Failed to add document") + addDocExpectation.fulfill() + } + } + self.wait(for: [addDocExpectation], timeout: TESTS_TIME_OUT) + } + + func testDeleteTask() { + let addDocExpectation = XCTestExpectation(description: "Add documents") + addDocuments(client: self.client, uid: self.uid, primaryKey: nil) { result in + switch result { + case .success(let task): + self.client.deleteTasks(filter: .init(uids: [task.uid])) { result in + switch result { + case .success(let taskInfo): + self.client.waitForTask(task: taskInfo) { _ in + self.client.getTasks { result in + switch result { + case .success(let tasks): + XCTAssertEqual(tasks.results[0].type, .taskDeletion) + XCTAssertNotEqual(tasks.results[1].uid, task.uid) + case .failure(let error): + dump(error) + XCTFail("Failed to get tasks") + } + + addDocExpectation.fulfill() + } + } + case .failure: + XCTFail("Failed to delete document") + addDocExpectation.fulfill() + } + } + case .failure: + XCTFail("Failed to add document") + addDocExpectation.fulfill() + } + } + self.wait(for: [addDocExpectation], timeout: TESTS_TIME_OUT) + } + func testWaitForTask() { let createIndexExpectation = XCTestExpectation(description: "Add documents") @@ -161,7 +226,7 @@ class TasksTests: XCTestCase { self.client.waitForTask(task: task, options: WaitOptions(timeOut: 1, interval: 0.5)) { result in switch result { case .success(let task): - XCTAssertEqual(task.type, "indexCreation") + XCTAssertEqual(task.type.description, "indexCreation") createIndexExpectation.fulfill() case .failure(let error): dump(error) @@ -186,7 +251,7 @@ class TasksTests: XCTestCase { self.client.waitForTask(taskUid: task.taskUid, options: WaitOptions(timeOut: 1, interval: 0.5)) { result in switch result { case .success(let task): - XCTAssertEqual(task.type, "indexCreation") + XCTAssertEqual(task.type.description, "indexCreation") createIndexExpectation.fulfill() case .failure(let error): dump(error) diff --git a/Tests/MeiliSearchIntegrationTests/Utils.swift b/Tests/MeiliSearchIntegrationTests/Utils.swift index a81207a3..8e780f8a 100644 --- a/Tests/MeiliSearchIntegrationTests/Utils.swift +++ b/Tests/MeiliSearchIntegrationTests/Utils.swift @@ -5,7 +5,6 @@ import Foundation import XCTest @testable import MeiliSearch -// swiftlint:disable force_try private let movies: [Movie] = [ Movie(id: 123, title: "Pride and Prejudice", comment: "A great book"), Movie(id: 456, title: "Le Petit Prince", comment: "A french book"), @@ -99,7 +98,13 @@ public func addDocuments(client: MeiliSearch, uid: String, primaryKey: String?, public func addDocuments(client: MeiliSearch, uid: String, dataset: [T], primaryKey: String?, _ completion: @escaping(Result) -> Void) { let jsonEncoder = JSONEncoder() - let documents: Data = try! jsonEncoder.encode(dataset) + let documents: Data + do { + documents = try jsonEncoder.encode(dataset) + } catch { + completion(.failure(error)) + return + } let index = client.index(uid) client.deleteIndex(uid) { result in diff --git a/Tests/MeiliSearchUnitTests/DocumentsTests.swift b/Tests/MeiliSearchUnitTests/DocumentsTests.swift index ccbf6555..0526b5e0 100755 --- a/Tests/MeiliSearchUnitTests/DocumentsTests.swift +++ b/Tests/MeiliSearchUnitTests/DocumentsTests.swift @@ -6,7 +6,7 @@ import Foundation #endif // swiftlint:disable force_unwrapping -// swiftlint:disable force_try + // swiftlint:disable line_length private struct Movie: Codable, Equatable { let id: Int @@ -28,21 +28,21 @@ class DocumentsTests: XCTestCase { private var uid: String = "movies_test" private let session = MockURLSession() - override func setUp() { - super.setUp() - client = try! MeiliSearch(host: "http://localhost:7700", apiKey: "masterKey", session: session) + override func setUpWithError() throws { + try super.setUpWithError() + client = try MeiliSearch(host: "http://localhost:7700", apiKey: "masterKey", session: session) index = self.client.index(self.uid) } - func testAddDocuments() { + func testAddDocuments() throws { let jsonString = """ {"taskUid":0,"indexUid":"books_test","status":"enqueued","type":"documentAdditionOrUpdate","enqueuedAt":"2022-07-21T21:47:50.565717794Z"} """ // Prepare the mock server - let decoder = JSONDecoder() + let decoder = Constants.customJSONDecoder let jsonData = jsonString.data(using: .utf8)! - let stubTask: TaskInfo = try! decoder.decode(TaskInfo.self, from: jsonData) + let stubTask: TaskInfo = try decoder.decode(TaskInfo.self, from: jsonData) session.pushData(jsonString, code: 202) // Start the test with the mocked server @@ -69,15 +69,15 @@ class DocumentsTests: XCTestCase { self.wait(for: [expectation], timeout: TESTS_TIME_OUT) } - func testAddDataDocuments() { + func testAddDataDocuments() throws { let jsonString = """ {"taskUid":0,"indexUid":"books_test","status":"enqueued","type":"documentAdditionOrUpdate","enqueuedAt":"2022-07-21T21:47:50.565717794Z"} """ // Prepare the mock server - let decoder = JSONDecoder() + let decoder = Constants.customJSONDecoder let jsonData = jsonString.data(using: .utf8)! - let stubTask: TaskInfo = try! decoder.decode(TaskInfo.self, from: jsonData) + let stubTask: TaskInfo = try decoder.decode(TaskInfo.self, from: jsonData) session.pushData(jsonString, code: 202) let documentJsonString = """ @@ -112,15 +112,15 @@ class DocumentsTests: XCTestCase { self.wait(for: [expectation], timeout: TESTS_TIME_OUT) } - func testUpdateDataDocuments() { + func testUpdateDataDocuments() throws { let jsonString = """ {"taskUid":0,"indexUid":"books_test","status":"enqueued","type":"documentAdditionOrUpdate","enqueuedAt":"2022-07-21T21:47:50.565717794Z"} """ // Prepare the mock server - let decoder = JSONDecoder() + let decoder = Constants.customJSONDecoder let jsonData = jsonString.data(using: .utf8)! - let stubTask: TaskInfo = try! decoder.decode(TaskInfo.self, from: jsonData) + let stubTask: TaskInfo = try decoder.decode(TaskInfo.self, from: jsonData) session.pushData(jsonString, code: 202) let documentJsonString = """ [{ @@ -149,15 +149,15 @@ class DocumentsTests: XCTestCase { self.wait(for: [expectation], timeout: TESTS_TIME_OUT) } - func testUpdateDocuments() { + func testUpdateDocuments() throws { let jsonString = """ {"taskUid":0,"indexUid":"books_test","status":"enqueued","type":"documentAdditionOrUpdate","enqueuedAt":"2022-07-21T21:47:50.565717794Z"} """ // Prepare the mock server - let decoder = JSONDecoder() + let decoder = Constants.customJSONDecoder let jsonData = jsonString.data(using: .utf8)! - let stubTask: TaskInfo = try! decoder.decode(TaskInfo.self, from: jsonData) + let stubTask: TaskInfo = try decoder.decode(TaskInfo.self, from: jsonData) session.pushData(jsonString, code: 202) let movie = Movie( @@ -185,7 +185,7 @@ class DocumentsTests: XCTestCase { self.wait(for: [expectation], timeout: TESTS_TIME_OUT) } - func testGetDocument() { + func testGetDocument() throws { let jsonString = """ { "id": 25684, @@ -198,10 +198,10 @@ class DocumentsTests: XCTestCase { // Prepare the mock server session.pushData(jsonString, code: 200) - let decoder = JSONDecoder() + let decoder = Constants.customJSONDecoder decoder.dateDecodingStrategy = .formatted(Formatter.iso8601) let data = jsonString.data(using: .utf8)! - let stubMovie: Movie = try! decoder.decode(Movie.self, from: data) + let stubMovie: Movie = try decoder.decode(Movie.self, from: data) let identifier: String = "25684" // Start the test with the mocked server @@ -232,7 +232,7 @@ class DocumentsTests: XCTestCase { session.pushData(jsonString, code: 200) do { - let decoder = JSONDecoder() + let decoder = Constants.customJSONDecoder decoder.dateDecodingStrategy = .formatted(Formatter.iso8601) let data = jsonString.data(using: .utf8)! let stubMovie: Movie = try decoder.decode(Movie.self, from: data) @@ -289,7 +289,7 @@ class DocumentsTests: XCTestCase { self.wait(for: [expectation], timeout: TESTS_TIME_OUT) } - func testGetDocuments() { + func testGetDocuments() throws { let jsonString = """ { "results": [ @@ -316,10 +316,10 @@ class DocumentsTests: XCTestCase { // Prepare the mock server session.pushData(jsonString, code: 200) - let decoder = JSONDecoder() + let decoder = Constants.customJSONDecoder decoder.dateDecodingStrategy = .formatted(Formatter.iso8601) let data = jsonString.data(using: .utf8)! - let stubMovies: DocumentsResults = try! decoder.decode(DocumentsResults.self, from: data) + let stubMovies: DocumentsResults = try decoder.decode(DocumentsResults.self, from: data) // Start the test with the mocked server let expectation = XCTestExpectation(description: "Get Movies documents") @@ -336,15 +336,15 @@ class DocumentsTests: XCTestCase { self.wait(for: [expectation], timeout: TESTS_TIME_OUT) } - func testDeleteDocument() { + func testDeleteDocument() throws { let jsonString = """ {"taskUid":0,"indexUid":"books_test","status":"enqueued","type":"documentAdditionOrUpdate","enqueuedAt":"2022-07-21T21:47:50.565717794Z"} """ // Prepare the mock server - let decoder = JSONDecoder() + let decoder = Constants.customJSONDecoder let jsonData = jsonString.data(using: .utf8)! - let stubTask: TaskInfo = try! decoder.decode(TaskInfo.self, from: jsonData) + let stubTask: TaskInfo = try decoder.decode(TaskInfo.self, from: jsonData) session.pushData(jsonString, code: 202) let identifier: String = "25684" @@ -364,15 +364,15 @@ class DocumentsTests: XCTestCase { self.wait(for: [expectation], timeout: TESTS_TIME_OUT) } - func testDeleteAllDocuments() { + func testDeleteAllDocuments() throws { let jsonString = """ {"taskUid":0,"indexUid":"books_test","status":"enqueued","type":"documentAdditionOrUpdate","enqueuedAt":"2022-07-21T21:47:50.565717794Z"} """ // Prepare the mock server - let decoder = JSONDecoder() + let decoder = Constants.customJSONDecoder let jsonData = jsonString.data(using: .utf8)! - let stubTask: TaskInfo = try! decoder.decode(TaskInfo.self, from: jsonData) + let stubTask: TaskInfo = try decoder.decode(TaskInfo.self, from: jsonData) session.pushData(jsonString, code: 202) // Start the test with the mocked server @@ -390,15 +390,15 @@ class DocumentsTests: XCTestCase { self.wait(for: [expectation], timeout: TESTS_TIME_OUT) } - func testDeleteBatchDocuments() { + func testDeleteBatchDocuments() throws { let jsonString = """ {"taskUid":0,"indexUid":"books_test","status":"enqueued","type":"documentAdditionOrUpdate","enqueuedAt":"2022-07-21T21:47:50.565717794Z"} """ // Prepare the mock server - let decoder = JSONDecoder() + let decoder = Constants.customJSONDecoder let jsonData = jsonString.data(using: .utf8)! - let stubTask: TaskInfo = try! decoder.decode(TaskInfo.self, from: jsonData) + let stubTask: TaskInfo = try decoder.decode(TaskInfo.self, from: jsonData) session.pushData(jsonString, code: 202) let documentsIdentifiers: [String] = ["23488", "153738", "437035", "363869"] @@ -418,15 +418,15 @@ class DocumentsTests: XCTestCase { } @available(*, deprecated, message: "Testing deprecated methods - marked deprecated to avoid additional warnings below.") - func testDeprecatedDeleteBatchDocuments() { + func testDeprecatedDeleteBatchDocuments() throws { let jsonString = """ {"taskUid":0,"indexUid":"books_test","status":"enqueued","type":"documentAdditionOrUpdate","enqueuedAt":"2022-07-21T21:47:50.565717794Z"} """ // Prepare the mock server - let decoder = JSONDecoder() + let decoder = Constants.customJSONDecoder let jsonData = jsonString.data(using: .utf8)! - let stubTask: TaskInfo = try! decoder.decode(TaskInfo.self, from: jsonData) + let stubTask: TaskInfo = try decoder.decode(TaskInfo.self, from: jsonData) session.pushData(jsonString, code: 202) let documentsIdentifiers: [Int] = [23488, 153738, 437035, 363869] @@ -447,5 +447,5 @@ class DocumentsTests: XCTestCase { } // swiftlint:enable force_unwrapping // swiftlint:enable force_cast -// swiftlint:enable force_try + // swiftlint:enable line_length diff --git a/Tests/MeiliSearchUnitTests/DumpsTests.swift b/Tests/MeiliSearchUnitTests/DumpsTests.swift index 56f6b694..f2d27660 100644 --- a/Tests/MeiliSearchUnitTests/DumpsTests.swift +++ b/Tests/MeiliSearchUnitTests/DumpsTests.swift @@ -2,17 +2,17 @@ import XCTest // swiftlint:disable force_unwrapping -// swiftlint:disable force_try + class DumpsTests: XCTestCase { private var client: MeiliSearch! private let session = MockURLSession() - override func setUp() { - super.setUp() - client = try! MeiliSearch(host: "http://localhost:7700", apiKey: "masterKey", session: session) + override func setUpWithError() throws { + try super.setUpWithError() + client = try MeiliSearch(host: "http://localhost:7700", apiKey: "masterKey", session: session) } - func testCreateDump() { + func testCreateDump() throws { // Prepare the mock server let json = """ @@ -21,7 +21,7 @@ class DumpsTests: XCTestCase { let data = json.data(using: .utf8)! - let stubDump: TaskInfo = try! Constants.customJSONDecoder.decode(TaskInfo.self, from: data) + let stubDump: TaskInfo = try Constants.customJSONDecoder.decode(TaskInfo.self, from: data) session.pushData(json) @@ -43,4 +43,3 @@ class DumpsTests: XCTestCase { } } // swiftlint:enable force_unwrapping -// swiftlint:enable force_try diff --git a/Tests/MeiliSearchUnitTests/IndexesTests.swift b/Tests/MeiliSearchUnitTests/IndexesTests.swift index c32c1f4b..a2ea1133 100755 --- a/Tests/MeiliSearchUnitTests/IndexesTests.swift +++ b/Tests/MeiliSearchUnitTests/IndexesTests.swift @@ -1,16 +1,15 @@ @testable import MeiliSearch import XCTest -// swiftlint:disable force_try class IndexesTests: XCTestCase { private var client: MeiliSearch! private var index: Indexes! private let uid: String = "movies_test" private let session = MockURLSession() - override func setUp() { - super.setUp() - client = try! MeiliSearch(host: "http://localhost:7700", apiKey: "masterKey", session: session) + override func setUpWithError() throws { + try super.setUpWithError() + client = try MeiliSearch(host: "http://localhost:7700", apiKey: "masterKey", session: session) index = client.index(self.uid) } @@ -240,7 +239,8 @@ class IndexesTests: XCTestCase { "results": [], "limit": 20, "from": 5, - "next": 98 + "next": 98, + "total": 6 } """ @@ -250,7 +250,7 @@ class IndexesTests: XCTestCase { // Start the test with the mocked server let expectation = XCTestExpectation(description: "Get keys with parameters") - self.index.getTasks(params: TasksQuery(limit: 20, from: 5, next: 98, types: ["indexCreation"])) { result in + self.index.getTasks(params: TasksQuery(limit: 20, from: 5, next: 98, types: [.indexCreation])) { result in switch result { case .success: let requestQuery = self.session.nextDataTask.request?.url?.query @@ -320,4 +320,3 @@ class IndexesTests: XCTestCase { } } // swiftlint:enable force_unwrapping -// swiftlint:enable force_try diff --git a/Tests/MeiliSearchUnitTests/KeysTests.swift b/Tests/MeiliSearchUnitTests/KeysTests.swift index 9db15635..9eeda04b 100644 --- a/Tests/MeiliSearchUnitTests/KeysTests.swift +++ b/Tests/MeiliSearchUnitTests/KeysTests.swift @@ -1,17 +1,15 @@ @testable import MeiliSearch import XCTest -// swiftlint:disable force_try class KeysTests: XCTestCase { private var client: MeiliSearch! private var index: Indexes! private let uid: String = "movies_test" private let session = MockURLSession() - override func setUp() { - super.setUp() - - client = try! MeiliSearch(host: "http://localhost:7700", apiKey: "masterKey", session: session) + override func setUpWithError() throws { + try super.setUpWithError() + client = try MeiliSearch(host: "http://localhost:7700", apiKey: "masterKey", session: session) index = client.index(self.uid) } @@ -50,4 +48,3 @@ class KeysTests: XCTestCase { } } // swiftlint:enable force_unwrapping -// swiftlint:enable force_try diff --git a/Tests/MeiliSearchUnitTests/QueryParameters/TasksQueryTests.swift b/Tests/MeiliSearchUnitTests/QueryParameters/TasksQueryTests.swift index 78a5ca4c..b2fe61d1 100644 --- a/Tests/MeiliSearchUnitTests/QueryParameters/TasksQueryTests.swift +++ b/Tests/MeiliSearchUnitTests/QueryParameters/TasksQueryTests.swift @@ -8,7 +8,7 @@ class TasksQueryTests: XCTestCase { func testRenderedQuery() { let data: [[String: TasksQuery]] = [ ["?limit=2": TasksQuery(limit: 2)], - ["?from=99&limit=2&types=name,title": TasksQuery(limit: 2, from: 99, types: ["name", "title"])], + ["?from=99&limit=2&types=indexSwap,dumpCreation": TasksQuery(limit: 2, from: 99, types: [.indexSwap, .dumpCreation])], ["?limit=2": TasksQuery(limit: 2, next: nil)], ["?from=2": TasksQuery(from: 2)], ["?indexUids=my-index,123": TasksQuery(indexUids: ["my-index", "123"])], diff --git a/Tests/MeiliSearchUnitTests/SearchTests.swift b/Tests/MeiliSearchUnitTests/SearchTests.swift index 506b7caa..cd9ef8e3 100644 --- a/Tests/MeiliSearchUnitTests/SearchTests.swift +++ b/Tests/MeiliSearchUnitTests/SearchTests.swift @@ -2,7 +2,7 @@ import XCTest // swiftlint:disable force_unwrapping -// swiftlint:disable force_try + private struct Movie: Codable, Equatable { let id: Int let title: String @@ -24,13 +24,13 @@ class SearchTests: XCTestCase { private var index: Indexes! private let session = MockURLSession() - override func setUp() { - super.setUp() - client = try! MeiliSearch(host: "http://localhost:7700", apiKey: "masterKey", session: session) + override func setUpWithError() throws { + try super.setUpWithError() + client = try MeiliSearch(host: "http://localhost:7700", apiKey: "masterKey", session: session) index = client.index("movies_test") } - func testSearchForBotmanMovie() { + func testSearchForBotmanMovie() throws { let jsonString = """ { "hits": [ @@ -59,7 +59,7 @@ class SearchTests: XCTestCase { // Prepare the mock server let data = jsonString.data(using: .utf8)! - let stubSearchResult: Searchable = try! Constants.customJSONDecoder.decode(Searchable.self, from: data) + let stubSearchResult: Searchable = try Constants.customJSONDecoder.decode(Searchable.self, from: data) session.pushData(jsonString) // Start the test with the mocked server @@ -80,8 +80,48 @@ class SearchTests: XCTestCase { self.wait(for: [expectation], timeout: TESTS_TIME_OUT) } + + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) + func testSearchForBotmanMovieAsync() async throws { + let jsonString = """ + { + "hits": [ + { + "id": 29751, + "title": "Batman Unmasked: The Psychology of the Dark Knight", + "poster": "https://image.tmdb.org/t/p/w1280/jjHu128XLARc2k4cJrblAvZe0HE.jpg", + "overview": "Delve into the world of Batman and the vigilante justice tha", + "release_date": "2020-04-04T19:59:49.259572Z" + }, + { + "id": 471474, + "title": "Batman: Gotham by Gaslight", + "poster": "https://image.tmdb.org/t/p/w1280/7souLi5zqQCnpZVghaXv0Wowi0y.jpg", + "overview": "ve Victorian Age Gotham City, Batman begins his war on crime", + "release_date": "2020-04-04T19:59:49.259572Z" + } + ], + "offset": 0, + "limit": 20, + "processingTimeMs": 2, + "estimatedTotalHits": 2, + "query": "botman" + } + """ + + // Prepare the mock server + let data = jsonString.data(using: .utf8)! + let stubSearchResult: Searchable = try! Constants.customJSONDecoder.decode(Searchable.self, from: data) + session.pushData(jsonString) + + // Start the test with the mocked server + let searchParameters = SearchParameters.query("botman") + + let searchResult: Searchable = try await self.index.search(searchParameters) + XCTAssertEqual(stubSearchResult, searchResult) + } - func testSearchForBotmanMovieFacets() { + func testSearchForBotmanMovieFacets() throws { let jsonString = """ { "hits": [ @@ -110,7 +150,7 @@ class SearchTests: XCTestCase { // Prepare the mock server let data = jsonString.data(using: .utf8)! - let stubSearchResult: Searchable = try! Constants.customJSONDecoder.decode(Searchable.self, from: data) + let stubSearchResult: Searchable = try Constants.customJSONDecoder.decode(Searchable.self, from: data) session.pushData(jsonString) // Start the test with the mocked server @@ -247,7 +287,7 @@ class SearchTests: XCTestCase { wait(for: [expectation], timeout: TESTS_TIME_OUT) } - func testSearchWithFinitePagination() { + func testSearchWithFinitePagination() throws { let jsonString = """ { "hits": [ @@ -277,7 +317,7 @@ class SearchTests: XCTestCase { // Prepare the mock server let data = jsonString.data(using: .utf8)! - let stubSearchResult: Searchable = try! Constants.customJSONDecoder.decode(Searchable.self, from: data) + let stubSearchResult: Searchable = try Constants.customJSONDecoder.decode(Searchable.self, from: data) session.pushData(jsonString) // Start the test with the mocked server @@ -308,4 +348,3 @@ class SearchTests: XCTestCase { } } // swiftlint:enable force_unwrapping -// swiftlint:enable force_try diff --git a/Tests/MeiliSearchUnitTests/SettingsTests.swift b/Tests/MeiliSearchUnitTests/SettingsTests.swift index 5299818a..9edd3d44 100644 --- a/Tests/MeiliSearchUnitTests/SettingsTests.swift +++ b/Tests/MeiliSearchUnitTests/SettingsTests.swift @@ -39,6 +39,15 @@ class SettingsTests: XCTestCase { "synonyms": { "wolverine": ["xmen", "logan"], "logan": ["wolverine", "xmen"] + }, + "typoTolerance": { + "enabled": true, + "minWordSizeForTypos": { + "oneTypo": 5, + "twoTypos": 9 + }, + "disableOnWords": [], + "disableOnAttributes": [] } } """ @@ -77,7 +86,7 @@ class SettingsTests: XCTestCase { """ // Prepare the mock server - let decoder = JSONDecoder() + let decoder = Constants.customJSONDecoder let jsonData = Data(jsonString.utf8) let stubTask: TaskInfo = try decoder.decode(TaskInfo.self, from: jsonData) @@ -106,7 +115,7 @@ class SettingsTests: XCTestCase { """ // Prepare the mock server - let decoder = JSONDecoder() + let decoder = Constants.customJSONDecoder let data: Data = Data(jsonString.utf8) let stubTask: TaskInfo = try decoder.decode(TaskInfo.self, from: data) session.pushData(jsonString) @@ -167,7 +176,7 @@ class SettingsTests: XCTestCase { """ // Prepare the mock server - let decoder = JSONDecoder() + let decoder = Constants.customJSONDecoder let stubTask: TaskInfo = try decoder.decode( TaskInfo.self, from: Data(jsonString.utf8)) @@ -205,7 +214,7 @@ class SettingsTests: XCTestCase { """ // Prepare the mock server - let decoder = JSONDecoder() + let decoder = Constants.customJSONDecoder let stubTask: TaskInfo = try decoder.decode( TaskInfo.self, from: Data(jsonString.utf8)) @@ -262,7 +271,7 @@ class SettingsTests: XCTestCase { """ // Prepare the mock server - let decoder = JSONDecoder() + let decoder = Constants.customJSONDecoder let stubTask: TaskInfo = try decoder.decode( TaskInfo.self, from: Data(jsonString.utf8)) @@ -295,7 +304,7 @@ class SettingsTests: XCTestCase { """ // Prepare the mock server - let decoder = JSONDecoder() + let decoder = Constants.customJSONDecoder let stubTask: TaskInfo = try decoder.decode( TaskInfo.self, from: Data(jsonString.utf8)) @@ -361,7 +370,7 @@ class SettingsTests: XCTestCase { """ // Prepare the mock server - let decoder = JSONDecoder() + let decoder = Constants.customJSONDecoder let stubTask: TaskInfo = try decoder.decode( TaskInfo.self, from: Data(jsonString.utf8)) @@ -394,7 +403,7 @@ class SettingsTests: XCTestCase { """ // Prepare the mock server - let decoder = JSONDecoder() + let decoder = Constants.customJSONDecoder let stubTask: TaskInfo = try decoder.decode( TaskInfo.self, from: Data(jsonString.utf8)) @@ -449,7 +458,7 @@ class SettingsTests: XCTestCase { """ // Prepare the mock server - let decoder = JSONDecoder() + let decoder = Constants.customJSONDecoder let stubTask: TaskInfo = try decoder.decode( TaskInfo.self, from: Data(jsonString.utf8)) @@ -478,7 +487,7 @@ class SettingsTests: XCTestCase { """ // Prepare the mock server - let decoder = JSONDecoder() + let decoder = Constants.customJSONDecoder let stubTask: TaskInfo = try decoder.decode( TaskInfo.self, from: Data(jsonString.utf8)) @@ -535,7 +544,7 @@ class SettingsTests: XCTestCase { """ // Prepare the mock server - let decoder = JSONDecoder() + let decoder = Constants.customJSONDecoder let stubTask: TaskInfo = try decoder.decode( TaskInfo.self, from: Data(jsonString.utf8)) @@ -569,7 +578,7 @@ class SettingsTests: XCTestCase { """ // Prepare the mock server - let decoder = JSONDecoder() + let decoder = Constants.customJSONDecoder let stubTask: TaskInfo = try decoder.decode( TaskInfo.self, from: Data(jsonString.utf8)) @@ -625,7 +634,7 @@ class SettingsTests: XCTestCase { """ // Prepare the mock server - let decoder = JSONDecoder() + let decoder = Constants.customJSONDecoder let stubTask: TaskInfo = try decoder.decode( TaskInfo.self, from: Data(jsonString.utf8)) @@ -662,7 +671,7 @@ class SettingsTests: XCTestCase { """ // Prepare the mock server - let decoder = JSONDecoder() + let decoder = Constants.customJSONDecoder let stubTask: TaskInfo = try decoder.decode( TaskInfo.self, from: Data(jsonString.utf8)) @@ -716,7 +725,7 @@ class SettingsTests: XCTestCase { """ // Prepare the mock server - let decoder = JSONDecoder() + let decoder = Constants.customJSONDecoder let stubTask: TaskInfo = try decoder.decode( TaskInfo.self, from: Data(jsonString.utf8)) @@ -746,7 +755,7 @@ class SettingsTests: XCTestCase { """ // Prepare the mock server - let decoder = JSONDecoder() + let decoder = Constants.customJSONDecoder let stubTask: TaskInfo = try decoder.decode( TaskInfo.self, from: Data(jsonString.utf8)) @@ -801,7 +810,7 @@ class SettingsTests: XCTestCase { """ // Prepare the mock server - let decoder = JSONDecoder() + let decoder = Constants.customJSONDecoder let stubTask: TaskInfo = try decoder.decode( TaskInfo.self, from: Data(jsonString.utf8)) @@ -831,7 +840,7 @@ class SettingsTests: XCTestCase { """ // Prepare the mock server - let decoder = JSONDecoder() + let decoder = Constants.customJSONDecoder let stubTask: TaskInfo = try decoder.decode( TaskInfo.self, from: Data(jsonString.utf8)) @@ -853,15 +862,99 @@ class SettingsTests: XCTestCase { self.wait(for: [expectation], timeout: TESTS_TIME_OUT) } + // MARK: Typo Tolerance + + func testGetTypoTolerance() { + let jsonString = """ + {"enabled":true,"minWordSizeForTypos":{"oneTypo":3,"twoTypos":7},"disableOnWords":["of", "the"],"disableOnAttributes":["genre"]} + """ + + // Prepare the mock server + session.pushData(jsonString) + + // Start the test with the mocked server + let expectation = XCTestExpectation(description: "Get displayed attribute") + + self.index.getTypoTolerance { result in + switch result { + case .success(let typoTolerance): + XCTAssertTrue(typoTolerance.enabled) + XCTAssertEqual(typoTolerance.minWordSizeForTypos.oneTypo, 3) + XCTAssertEqual(typoTolerance.minWordSizeForTypos.twoTypos, 7) + XCTAssertFalse(typoTolerance.disableOnWords.isEmpty) + XCTAssertFalse(typoTolerance.disableOnAttributes.isEmpty) + case .failure: + XCTFail("Failed to get displayed attribute") + } + expectation.fulfill() + } + + self.wait(for: [expectation], timeout: TESTS_TIME_OUT) + } + + func testUpdateTypoTolerance() throws { + let jsonString = """ + {"taskUid":0,"indexUid":"movies_test","status":"enqueued","type":"settingsUpdate","enqueuedAt":"2022-07-27T19:03:50.494232841Z"} + """ + + // Prepare the mock server + let decoder = Constants.customJSONDecoder + let stubTask: TaskInfo = try decoder.decode(TaskInfo.self, from: Data(jsonString.utf8)) + + session.pushData(jsonString) + let typoTolerance: TypoTolerance = .init(enabled: false) + + // Start the test with the mocked server + let expectation = XCTestExpectation(description: "Update displayed attribute") + + self.index.updateTypoTolerance(typoTolerance) { result in + switch result { + case .success(let update): + XCTAssertEqual(stubTask, update) + case .failure: + XCTFail("Failed to update displayed attribute") + } + expectation.fulfill() + } + + self.wait(for: [expectation], timeout: TESTS_TIME_OUT) + } + + func testResetTypoTolerance() throws { + let jsonString = """ + {"taskUid":0,"indexUid":"movies_test","status":"enqueued","type":"settingsUpdate","enqueuedAt":"2022-07-27T19:03:50.494232841Z"} + """ + + // Prepare the mock server + let decoder = Constants.customJSONDecoder + let stubTask: TaskInfo = try decoder.decode(TaskInfo.self, from: Data(jsonString.utf8)) + session.pushData(jsonString) + + // Start the test with the mocked server + let expectation = XCTestExpectation(description: "Update displayed attribute") + + self.index.resetTypoTolerance { result in + switch result { + case .success(let update): + XCTAssertEqual(stubTask, update) + case .failure: + XCTFail("Failed to update displayed attribute") + } + expectation.fulfill() + } + + self.wait(for: [expectation], timeout: TESTS_TIME_OUT) + } + private func buildStubSetting(from json: String) throws -> Setting { let data = Data(json.utf8) - let decoder = JSONDecoder() + let decoder = Constants.customJSONDecoder return try decoder.decode(Setting.self, from: data) } private func buildStubSettingResult(from json: String) throws -> SettingResult { let data = Data(json.utf8) - let decoder = JSONDecoder() + let decoder = Constants.customJSONDecoder return try decoder.decode(SettingResult.self, from: data) } } diff --git a/Tests/MeiliSearchUnitTests/StatsTests.swift b/Tests/MeiliSearchUnitTests/StatsTests.swift index d9e509c0..601284c5 100755 --- a/Tests/MeiliSearchUnitTests/StatsTests.swift +++ b/Tests/MeiliSearchUnitTests/StatsTests.swift @@ -2,20 +2,20 @@ import XCTest // swiftlint:disable force_unwrapping -// swiftlint:disable force_try + class StatsTests: XCTestCase { private var client: MeiliSearch! private var index: Indexes! private var uid: String = "movies_test" private let session = MockURLSession() - override func setUp() { - super.setUp() - client = try! MeiliSearch(host: "http://localhost:7700", apiKey: "masterKey", session: session) + override func setUpWithError() throws { + try super.setUpWithError() + client = try MeiliSearch(host: "http://localhost:7700", apiKey: "masterKey", session: session) index = client.index(self.uid) } - func testStats() { + func testStats() throws { let jsonString = """ { "numberOfDocuments": 19654, @@ -32,7 +32,7 @@ class StatsTests: XCTestCase { // Prepare the mock server let jsonData = jsonString.data(using: .utf8)! - let stubStats: Stat = try! Constants.customJSONDecoder.decode(Stat.self, from: jsonData) + let stubStats: Stat = try Constants.customJSONDecoder.decode(Stat.self, from: jsonData) session.pushData(jsonString) @@ -51,7 +51,7 @@ class StatsTests: XCTestCase { self.wait(for: [expectation], timeout: TESTS_TIME_OUT) } - func testAllStats() { + func testAllStats() throws { let jsonString = """ { "databaseSize": 447819776, @@ -82,7 +82,7 @@ class StatsTests: XCTestCase { """ // Prepare the mock server let jsonData = jsonString.data(using: .utf8)! - let stubAllStats: AllStats = try! Constants.customJSONDecoder.decode(AllStats.self, from: jsonData) + let stubAllStats: AllStats = try Constants.customJSONDecoder.decode(AllStats.self, from: jsonData) session.pushData(jsonString) @@ -103,4 +103,3 @@ class StatsTests: XCTestCase { } } // swiftlint:enable force_unwrapping -// swiftlint:enable force_try diff --git a/Tests/MeiliSearchUnitTests/SystemTests.swift b/Tests/MeiliSearchUnitTests/SystemTests.swift index b0f44c53..0fa08ac4 100755 --- a/Tests/MeiliSearchUnitTests/SystemTests.swift +++ b/Tests/MeiliSearchUnitTests/SystemTests.swift @@ -2,18 +2,18 @@ import XCTest // swiftlint:disable force_unwrapping -// swiftlint:disable force_try + class SystemTests: XCTestCase { private var client: MeiliSearch! private let session = MockURLSession() - override func setUp() { - super.setUp() - client = try! MeiliSearch(host: "http://localhost:7700", apiKey: "masterKey", session: session) + override func setUpWithError() throws { + try super.setUpWithError() + client = try MeiliSearch(host: "http://localhost:7700", apiKey: "masterKey", session: session) } - func testHealthStatusAvailable() { + func testHealthStatusAvailable() throws { // Prepare the mock server let jsonString = """ @@ -24,7 +24,7 @@ class SystemTests: XCTestCase { let jsonData = jsonString.data(using: .utf8)! - let expectedHealthBody: Health = try! Constants.customJSONDecoder.decode(Health.self, from: jsonData) + let expectedHealthBody: Health = try Constants.customJSONDecoder.decode(Health.self, from: jsonData) session.pushData(jsonString, code: 200) @@ -93,7 +93,7 @@ class SystemTests: XCTestCase { self.wait(for: [expectation], timeout: TESTS_TIME_OUT) } - func testVersion() { + func testVersion() throws { // Prepare the mock server let jsonString = """ @@ -106,7 +106,7 @@ class SystemTests: XCTestCase { let jsonData = jsonString.data(using: .utf8)! - let stubVersion: Version = try! Constants.customJSONDecoder.decode(Version.self, from: jsonData) + let stubVersion: Version = try Constants.customJSONDecoder.decode(Version.self, from: jsonData) session.pushData(jsonString) @@ -128,4 +128,3 @@ class SystemTests: XCTestCase { } } // swiftlint:enable force_unwrapping -// swiftlint:enable force_try diff --git a/Tests/MeiliSearchUnitTests/TasksTests.swift b/Tests/MeiliSearchUnitTests/TasksTests.swift index d45df6f3..a27a5f69 100644 --- a/Tests/MeiliSearchUnitTests/TasksTests.swift +++ b/Tests/MeiliSearchUnitTests/TasksTests.swift @@ -1,16 +1,15 @@ @testable import MeiliSearch import XCTest -// swiftlint:disable force_try class TasksTests: XCTestCase { private var client: MeiliSearch! private var index: Indexes! private let uid: String = "movies_test" private let session = MockURLSession() - override func setUp() { - super.setUp() - client = try! MeiliSearch(host: "http://localhost:7700", apiKey: "masterKey", session: session) + override func setUpWithError() throws { + try super.setUpWithError() + client = try MeiliSearch(host: "http://localhost:7700", apiKey: "masterKey", session: session) index = client.index(self.uid) } @@ -20,7 +19,8 @@ class TasksTests: XCTestCase { "results": [], "limit": 20, "from": 5, - "next": 98 + "next": 98, + "total": 6 } """ @@ -30,7 +30,7 @@ class TasksTests: XCTestCase { // Start the test with the mocked server let expectation = XCTestExpectation(description: "Get keys with parameters") - self.client.getTasks(params: TasksQuery(limit: 20, from: 5, next: 98, types: ["indexCreation"])) { result in + self.client.getTasks(params: TasksQuery(limit: 20, from: 5, next: 98, types: [.indexCreation])) { result in switch result { case .success: let requestQuery = self.session.nextDataTask.request?.url?.query @@ -49,4 +49,3 @@ class TasksTests: XCTestCase { } } // swiftlint:enable force_unwrapping -// swiftlint:enable force_try