Skip to content

Commit

Permalink
feat(HitsInteractor): ability to set a custom json decoder to hits in…
Browse files Browse the repository at this point in the history
…teractor (#254)

* feat: ability to set a custom json decoder to hits interactor
  • Loading branch information
VladislavFitz committed Sep 29, 2022
1 parent ef64115 commit 91628d7
Show file tree
Hide file tree
Showing 5 changed files with 66 additions and 14 deletions.
19 changes: 13 additions & 6 deletions Sources/InstantSearchCore/Hits/HitsExtractable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,26 +10,33 @@ import Foundation

public protocol HitsExtractable {

func extractHits<T: Decodable>() throws -> [T]
func extractHits<T: Decodable>(jsonDecoder: JSONDecoder) throws -> [T]

}

extension SearchResponse: HitsExtractable {}
extension SearchResponse: HitsExtractable {

public func extractHits<T>(jsonDecoder: JSONDecoder) throws -> [T] where T: Decodable {
let hitsData = try JSONEncoder().encode(hits)
return try jsonDecoder.decode([T].self, from: hitsData)
}

}

extension PlacesResponse: HitsExtractable {

public func extractHits<T>() throws -> [T] where T: Decodable {
public func extractHits<T>(jsonDecoder: JSONDecoder) throws -> [T] where T: Decodable {
let hitsData = try JSONEncoder().encode(hits)
return try JSONDecoder().decode([T].self, from: hitsData)
return try jsonDecoder.decode([T].self, from: hitsData)
}

}

extension FacetSearchResponse: HitsExtractable {

public func extractHits<T>() throws -> [T] where T: Decodable {
public func extractHits<T>(jsonDecoder: JSONDecoder) throws -> [T] where T: Decodable {
let hitsData = try JSONEncoder().encode(facetHits)
return try JSONDecoder().decode([T].self, from: hitsData)
return try jsonDecoder.decode([T].self, from: hitsData)
}

}
18 changes: 13 additions & 5 deletions Sources/InstantSearchCore/Hits/HitsInteractor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,17 +57,23 @@ public class HitsInteractor<Record: Codable>: AnyHitsInteractor {
}
}

/// JSONDecoder used for hits decoding from the search response
public var jsonDecoder: JSONDecoder

convenience public init(infiniteScrolling: InfiniteScrolling = Constants.Defaults.infiniteScrolling,
showItemsOnEmptyQuery: Bool = Constants.Defaults.showItemsOnEmptyQuery) {
showItemsOnEmptyQuery: Bool = Constants.Defaults.showItemsOnEmptyQuery,
jsonDecoder: JSONDecoder = JSONDecoder()) {
let settings = Settings(infiniteScrolling: infiniteScrolling,
showItemsOnEmptyQuery: showItemsOnEmptyQuery)
self.init(settings: settings)
}

public convenience init(settings: Settings? = nil) {
public convenience init(settings: Settings? = nil,
jsonDecoder: JSONDecoder = JSONDecoder()) {
self.init(settings: settings,
paginationController: Paginator<Record>(),
infiniteScrollingController: InfiniteScrollingController())
infiniteScrollingController: InfiniteScrollingController(),
jsonDecoder: jsonDecoder)
Telemetry.shared.trace(type: .hits,
parameters: settings.flatMap { settings in
[
Expand All @@ -80,7 +86,8 @@ public class HitsInteractor<Record: Codable>: AnyHitsInteractor {

internal init(settings: Settings? = nil,
paginationController: Paginator<Record>,
infiniteScrollingController: InfiniteScrollable) {
infiniteScrollingController: InfiniteScrollable,
jsonDecoder: JSONDecoder = JSONDecoder()) {
self.settings = settings ?? Settings()
self.paginator = paginationController
self.infiniteScrollingController = infiniteScrollingController
Expand All @@ -90,6 +97,7 @@ public class HitsInteractor<Record: Codable>: AnyHitsInteractor {
self.mutationQueue = .init()
self.mutationQueue.maxConcurrentOperationCount = 1
self.mutationQueue.qualityOfService = .userInitiated
self.jsonDecoder = jsonDecoder
}

public func numberOfHits() -> Int {
Expand Down Expand Up @@ -222,7 +230,7 @@ extension HitsInteractor: ResultUpdatable {
hitsInteractor.isLastQueryEmpty = stats.query.isNilOrEmpty

do {
let page: HitsPage<Record> = try HitsPage(searchResults: searchResults)
let page: HitsPage<Record> = try HitsPage(searchResults: searchResults, jsonDecoder: hitsInteractor.jsonDecoder)
hitsInteractor.paginator.process(page)
hitsInteractor.onResultsUpdated.fire(searchResults)
} catch let error {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ struct HitsPage<Item: Codable>: Pageable {

extension HitsPage {

init(searchResults: HitsExtractable & SearchStatsConvertible) throws {
init(searchResults: HitsExtractable & SearchStatsConvertible, jsonDecoder: JSONDecoder) throws {
self.index = searchResults.searchStats.page
self.items = try searchResults.extractHits()
self.items = try searchResults.extractHits(jsonDecoder: jsonDecoder)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class HitsInteractorControllerConnectionTests: XCTestCase {
var interactor: HitsInteractor<JSON> {
return HitsInteractor<JSON>(settings: .init(infiniteScrolling: .on(withOffset: 10), showItemsOnEmptyQuery: true),
paginationController: .init(),
infiniteScrollingController: TestInfiniteScrollingController())
infiniteScrollingController: TestInfiniteScrollingController(), jsonDecoder: JSONDecoder())
}

weak var disposableController: TestHitsController<JSON>?
Expand Down
37 changes: 37 additions & 0 deletions Tests/InstantSearchCoreTests/Unit/Hits/HitsInteractorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -198,5 +198,42 @@ class HitsInteractorTests: XCTestCase {
waitForExpectations(timeout: 3, handler: nil)

}

struct Person: Codable, Equatable {
let firstName: String
let lastName: String
}

func testCustomJSONDecoder() throws {

let snakeCaseDecoder = JSONDecoder()
snakeCaseDecoder.keyDecodingStrategy = .convertFromSnakeCase

let hitsInteractor = HitsInteractor<Person>(
settings: .init(infiniteScrolling: .on(withOffset: 10), showItemsOnEmptyQuery: true),
paginationController: Paginator(),
infiniteScrollingController: TestInfiniteScrollingController(),
jsonDecoder: snakeCaseDecoder)

var response = SearchResponse(hits: [
try Hit(json: ["objectID": "1", "first_name": "Jack", "last_name": "Johnson"]),
try Hit(json: ["objectID": "2", "first_name": "Helen", "last_name": "Smith"]),
])
response.page = 0

hitsInteractor.update(response)

let exp = expectation(description: "on results updated")

hitsInteractor.onResultsUpdated.subscribe(with: self) { _, results in
XCTAssertEqual(hitsInteractor.hits.count, 2)
XCTAssertEqual(hitsInteractor.hit(atIndex: 0), Person(firstName: "Jack", lastName: "Johnson"))
XCTAssertEqual(hitsInteractor.hit(atIndex: 1), Person(firstName: "Helen", lastName: "Smith"))
exp.fulfill()
}

waitForExpectations(timeout: 5)

}

}

0 comments on commit 91628d7

Please sign in to comment.