Skip to content

Commit 47ae361

Browse files
fix(multi-search): crash in case of requests-results count mismatch (#261)
* fix: throw error in case of requests-results count mismatch in the multisearcher
1 parent 0c3bd4c commit 47ae361

File tree

5 files changed

+184
-18
lines changed

5 files changed

+184
-18
lines changed
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
//
2+
// Array+Ranges.swift
3+
//
4+
//
5+
// Created by Vladislav Fitc on 26/10/2022.
6+
//
7+
8+
import Foundation
9+
10+
extension Array where Element: RandomAccessCollection, Element.Index == Int {
11+
12+
/// Maps the nested lists to the ranges corresponding to the positions of the nested list elements in the flatten list
13+
/// Example: [["a", "b", "c"], ["d", "e"], ["f", "g", "h"]] -> [0..<3, 3..<5, 5..<8]
14+
func flatRanges() -> [Range<Int>] {
15+
var ranges: [Range<Int>] = []
16+
var offset: Int = 0
17+
for sublist in self {
18+
let nextOffset = offset+sublist.count
19+
let range = offset..<nextOffset
20+
ranges.append(range)
21+
offset = nextOffset
22+
}
23+
return ranges
24+
}
25+
26+
}

Sources/InstantSearchCore/Searcher/Multi/AbstractMultiSearcher.swift

Lines changed: 13 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -44,27 +44,22 @@ extension AbstractMultiSearcher: MultiSearchComponent {
4444

4545
let requests = requestsAndCompletions.map(\.requests)
4646
let completions = requestsAndCompletions.map(\.completion)
47-
48-
/// Maps the nested lists to the ranges corresponding to the positions of the nested list elements in the flatten list
49-
/// Example: [["a", "b", "c"], ["d", "e"], ["f", "g", "h"]] -> [0..<3, 3..<5, 5..<8]
50-
func flatRanges<T>(_ list: [[T]]) -> [Range<Int>] {
51-
var ranges: [Range<Int>] = []
52-
var offset: Int = 0
53-
for sublist in list {
54-
let nextOffset = offset+sublist.count
55-
let range = offset..<nextOffset
56-
ranges.append(range)
57-
offset = nextOffset
58-
}
59-
return ranges
60-
}
61-
62-
let rangePerCompletion = zip(completions, flatRanges(requests))
47+
let rangePerCompletion = zip(completions, requests.flatRanges())
6348

6449
return (requests.flatMap { $0 }, { result in
6550
for (completion, range) in rangePerCompletion {
66-
let resultForCompletion = result.map { Array($0[range]) }
67-
completion(resultForCompletion)
51+
switch result {
52+
case .success(let subresults):
53+
guard
54+
range.lowerBound <= subresults.endIndex,
55+
range.upperBound <= subresults.endIndex else {
56+
completion(.failure(MultiSearchError.resultsRangeMismatch(range, subresults.startIndex..<subresults.endIndex) ))
57+
return
58+
}
59+
completion(.success(Array(subresults[range])))
60+
case .failure(let error):
61+
completion(.failure(MultiSearchError.serviceError(error)))
62+
}
6863
}
6964
})
7065
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
//
2+
// MultiSearchError.swift
3+
//
4+
//
5+
// Created by Vladislav Fitc on 02/11/2022.
6+
//
7+
8+
import Foundation
9+
10+
public enum MultiSearchError: LocalizedError {
11+
case resultsRangeMismatch(Range<Int>, Range<Int>)
12+
case serviceError(Error)
13+
14+
var localizedDescription: String {
15+
switch self {
16+
case .serviceError(let error):
17+
return "Search service error: \(error.localizedDescription)"
18+
case .resultsRangeMismatch(let subRange, let range):
19+
return "The calculated results subrange \(subRange) can't be extracted from the results list with bounds \(range)"
20+
}
21+
}
22+
23+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
//
2+
// ArrayExtensionsTests.swift
3+
//
4+
//
5+
// Created by Vladislav Fitc on 26/10/2022.
6+
//
7+
8+
import Foundation
9+
import XCTest
10+
@testable import InstantSearchCore
11+
12+
class ArrayExtensionsTests: XCTestCase {
13+
14+
func testFlatRanges() {
15+
let input = [["a", "b", "c"], ["d", "e"], ["f", "g", "h"]]
16+
let output = [0..<3, 3..<5, 5..<8]
17+
XCTAssertEqual(input.flatRanges(), output)
18+
19+
XCTAssertEqual([["a"], ["b"], ["c"]].flatRanges(), [0..<1, 1..<2, 2..<3])
20+
XCTAssertEqual([["a"], ["b"], []].flatRanges(), [0..<1, 1..<2, 2..<2])
21+
XCTAssertEqual([["a"], [], ["c"]].flatRanges(), [0..<1, 1..<1, 1..<2])
22+
XCTAssertEqual([[], [], []].flatRanges(), [0..<0, 0..<0, 0..<0])
23+
XCTAssertEqual([["a", "b"], ["c"], ["d"]].flatRanges(), [0..<2, 2..<3, 3..<4])
24+
XCTAssertEqual([[String]]().flatRanges(), [])
25+
26+
27+
print(["a", "b", "c"][3..<3])
28+
}
29+
30+
31+
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
//
2+
// MultiSearcherTests.swift
3+
//
4+
//
5+
// Created by Vladislav Fitc on 27/10/2022.
6+
//
7+
8+
import Foundation
9+
import XCTest
10+
@testable import InstantSearchCore
11+
12+
class MultiSearcherTests: XCTestCase {
13+
14+
func testResultsDistribution() {
15+
let service = TestMultiSearchService()
16+
let searcher = AbstractMultiSearcher(service: service,
17+
initialRequest: .init())
18+
let subSearcher = TestMultiSearchComponent()
19+
subSearcher.requests = ["req1", "req2"]
20+
let anotherSubSearcher = TestMultiSearchComponent()
21+
anotherSubSearcher.requests = ["req3", "req4", "req5"]
22+
searcher.addSearcher(subSearcher)
23+
searcher.addSearcher(anotherSubSearcher)
24+
25+
26+
subSearcher.completion = { result in
27+
switch result {
28+
case .success(let results):
29+
XCTAssertEqual(results, ["res1", "res2"])
30+
case .failure(let error):
31+
XCTFail("Unexpected error: \(error)")
32+
}
33+
}
34+
35+
anotherSubSearcher.completion = { result in
36+
switch result {
37+
case .failure(MultiSearchError.resultsRangeMismatch(2..<5, 0..<4)):
38+
break
39+
case .failure(let error):
40+
XCTFail("Unexpected error: \(error)")
41+
case .success(let results):
42+
XCTFail("Unexpected success \(results)")
43+
}
44+
}
45+
46+
let (_, completion) = searcher.collect()
47+
48+
completion(.success(["res1", "res2", "res3", "res4"]))
49+
}
50+
51+
}
52+
53+
struct TestMultiRequest: MultiRequest {
54+
var subRequests: [String]
55+
56+
init(subRequests: [String] = []) {
57+
self.subRequests = subRequests
58+
}
59+
}
60+
61+
struct TestMultiResult: MultiResult {
62+
var subResults: [String]
63+
64+
init(subResults: [String] = []) {
65+
self.subResults = subResults
66+
}
67+
}
68+
69+
class TestMultiSearchComponent: MultiSearchComponent {
70+
71+
var requests: [String] = []
72+
var completion: (Result<[String], Error>) -> Void = { _ in }
73+
74+
init() {
75+
}
76+
77+
func collect() -> (requests: [String], completion: (Result<[String], Error>) -> Void) {
78+
return (requests: requests, completion: completion)
79+
}
80+
81+
}
82+
83+
class TestMultiSearchService: MultiSearchService {
84+
85+
func search(_ request: TestMultiRequest, completion: @escaping (Swift.Result<TestMultiResult, Error>) -> Void) -> Operation {
86+
return Operation()
87+
}
88+
89+
}
90+
91+

0 commit comments

Comments
 (0)