From 2ed62636e17fd131d29e6f008333461371f01f7c Mon Sep 17 00:00:00 2001 From: Jack Alto <384288+aokj4ck@users.noreply.github.com> Date: Tue, 9 Apr 2024 17:07:13 -0400 Subject: [PATCH] [SSDK-622] Add language parameter to offline tileset descriptor creation (#202) ### Description Fixes [SSDK-622](https://mapbox.atlassian.net/browse/SSDK-622) Add optional `language` parameter that has two modes: 1. Absent language parameter / `nil` value will use existing behavior of _just_ the dataset name. 2. A given language parameter will be appended to the dataset name. ### Checklist - [x] Update `CHANGELOG` - [x] Add a non-English language test after #199 is merged --- CHANGELOG.md | 3 + .../InternalAPI/CoreSearchEngineStatics.swift | 57 ++++++++++++++++-- .../Offline/SearchOfflineManager.swift | 37 ++++++++++-- .../OfflineIntegrationTests.swift | 59 ++++++++++++++++++- 4 files changed, 144 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ae47e89d..2471ab400 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ Guide: https://keepachangelog.com/en/1.0.0/ +- [Offline] Add optional `language` parameter to SearchOfflineManager.createTilesetDescriptor and SearchOfflineManager.createPlacesTilesetDescriptor functions. +- [Tests] Add Spanish language offline search test. + - [Offline] Added OfflineIndexObserver which accepts two blocks for indexChanged or error events. This can be assigned to the offline search engine to receive state updates. - [Offline] Change default tileset name to `mbx-main` diff --git a/Sources/MapboxSearch/InternalAPI/CoreSearchEngineStatics.swift b/Sources/MapboxSearch/InternalAPI/CoreSearchEngineStatics.swift index 71e4c3ffd..b7b53b270 100644 --- a/Sources/MapboxSearch/InternalAPI/CoreSearchEngineStatics.swift +++ b/Sources/MapboxSearch/InternalAPI/CoreSearchEngineStatics.swift @@ -1,11 +1,60 @@ import Foundation enum CoreSearchEngineStatics { - static func createTilesetDescriptor(dataset: String, version: String) -> MapboxCommon.TilesetDescriptor { - CoreSearchEngine.createTilesetDescriptor(forDataset: dataset, version: version) + enum Constants { + static let delimiter = "_" } - static func createPlacesTilesetDescriptor(dataset: String, version: String) -> MapboxCommon.TilesetDescriptor { - CoreSearchEngine.createPlacesTilesetDescriptor(forDataset: dataset, version: version) + static func createTilesetDescriptor(dataset: String, version: String, language: String? = nil) -> MapboxCommon + .TilesetDescriptor { + let identifier: String + if let language { + if ISOLanguages.contains(language: language) { + identifier = dataset + Constants.delimiter + language + } else { + _Logger.searchSDK + .warning( + "Provided language code '\(language)' for tileset is non-ISO. Dataset '\(dataset)' without language will be used." + ) + identifier = dataset + } + } else { + identifier = dataset + } + return CoreSearchEngine.createTilesetDescriptor(forDataset: identifier, version: version) + } + + static func createPlacesTilesetDescriptor(dataset: String, version: String, language: String? = nil) -> MapboxCommon + .TilesetDescriptor { + let identifier: String + if let language { + if ISOLanguages.contains(language: language) { + identifier = dataset + Constants.delimiter + language + } else { + _Logger.searchSDK + .warning( + "Provided language code '\(language)' for places tileset is non-ISO. Dataset '\(dataset)' without language will be used." + ) + identifier = dataset + } + } else { + identifier = dataset + } + return CoreSearchEngine.createPlacesTilesetDescriptor(forDataset: identifier, version: version) + } +} + +enum ISOLanguages { + static func contains(language: String) -> Bool { + var validLanguage: Bool + if #available(iOS 16, *) { + validLanguage = Locale.LanguageCode.isoLanguageCodes + .map(\.identifier) + .contains(language) + } else { + validLanguage = Locale.isoLanguageCodes + .contains(language) + } + return validLanguage } } diff --git a/Sources/MapboxSearch/PublicAPI/Offline/SearchOfflineManager.swift b/Sources/MapboxSearch/PublicAPI/Offline/SearchOfflineManager.swift index cf8a898cc..52fad5780 100644 --- a/Sources/MapboxSearch/PublicAPI/Offline/SearchOfflineManager.swift +++ b/Sources/MapboxSearch/PublicAPI/Offline/SearchOfflineManager.swift @@ -41,26 +41,53 @@ public class SearchOfflineManager { engine.setTileStore(searchTileStore.commonTileStore, completion: completion) } - /// Creates TilesetDescriptor for offline search index data with provided dataset name and version. + // MARK: - Tileset with name, version, and language parameters + + /// Creates TilesetDescriptor for offline search index data with provided dataset name, version, and language. + /// Providing nil or excluding the language parameter will use the dataset name as-is. + /// Providing a language will append it to the name. /// - Parameters: /// - dataset: dataset name /// - version: dataset version + /// - language: Provide a ISO 639-1 Code language from NSLocale. Values will be appended to the place dataset + /// name. /// - Returns: TilesetDescriptor for TileStore - public static func createTilesetDescriptor(dataset: String, version: String? = nil) -> MapboxCommon + public static func createTilesetDescriptor( + dataset: String, + version: String? = nil, + language: String? = nil + ) -> MapboxCommon .TilesetDescriptor { - CoreSearchEngineStatics.createTilesetDescriptor(dataset: dataset, version: version ?? "") + CoreSearchEngineStatics.createTilesetDescriptor( + dataset: dataset, + version: version ?? "", + language: language + ) } /// Creates TilesetDescriptor for offline search boundaries with provided dataset name and version. + /// Providing nil or excluding the language parameter will use the places dataset name as-is. + /// Providing a language will append it to the name. /// - Parameters: /// - dataset: dataset name /// - version: dataset version + /// - language: Provide a ISO 639-1 Code language from NSLocale. Values will be appended to the dataset name. /// - Returns: TilesetDescriptor for TileStore - public static func createPlacesTilesetDescriptor(dataset: String, version: String? = nil) -> MapboxCommon + public static func createPlacesTilesetDescriptor( + dataset: String, + version: String? = nil, + language: String? = nil + ) -> MapboxCommon .TilesetDescriptor { - CoreSearchEngineStatics.createPlacesTilesetDescriptor(dataset: dataset, version: version ?? "") + CoreSearchEngineStatics.createPlacesTilesetDescriptor( + dataset: dataset, + version: version ?? "", + language: language + ) } + // MARK: - Default tileset + /// Creates TilesetDescriptor for offline search index data using default dataset name. /// - Returns: TilesetDescriptor for TileStore public static func createDefaultTilesetDescriptor() -> MapboxCommon.TilesetDescriptor { diff --git a/Tests/MapboxSearchIntegrationTests/OfflineIntegrationTests.swift b/Tests/MapboxSearchIntegrationTests/OfflineIntegrationTests.swift index 200916951..b705f2262 100644 --- a/Tests/MapboxSearchIntegrationTests/OfflineIntegrationTests.swift +++ b/Tests/MapboxSearchIntegrationTests/OfflineIntegrationTests.swift @@ -36,10 +36,15 @@ class OfflineIntegrationTests: MockServerIntegrationTestCase { wait(for: [setTileStoreExpectation], timeout: 10) } - func loadData(completion: @escaping (Result) -> Void) + func loadData( + tilesetDescriptor: TilesetDescriptor? = nil, + completion: @escaping (Result) -> Void + ) -> SearchCancelable { - /// This will use the default dataset defined at ``SearchOfflineManager.defaultDatasetName`` - let descriptor = SearchOfflineManager.createDefaultTilesetDescriptor() + /// A nil tilesetDescriptor parameter will fallback to the default dataset defined at + /// ``SearchOfflineManager.defaultDatasetName`` + let descriptor = tilesetDescriptor ?? SearchOfflineManager.createDefaultTilesetDescriptor() + let dcLocationValue = NSValue(mkCoordinate: dcLocation) let options = MapboxCommon.TileRegionLoadOptions.build( geometry: Geometry(point: dcLocationValue), @@ -104,6 +109,54 @@ class OfflineIntegrationTests: MockServerIntegrationTestCase { XCTAssertFalse(searchEngine.suggestions.isEmpty) } + func testSpanishLanguageSupport() throws { + clearData() + + // Set up index observer before the fetch starts to validate changes after it completes + let indexChangedExpectation = expectation(description: "Received offline index changed event") + let offlineIndexObserver = OfflineIndexObserver(onIndexChangedBlock: { changeEvent in + _Logger.searchSDK.info("Index changed: \(changeEvent)") + indexChangedExpectation.fulfill() + }, onErrorBlock: { error in + _Logger.searchSDK.error("Encountered error in OfflineIndexObserver \(error)") + XCTFail(error.debugDescription) + }) + searchEngine.offlineManager.engine.addOfflineIndexObserver(for: offlineIndexObserver) + + // Perform the offline fetch + let spanishTileset = SearchOfflineManager.createTilesetDescriptor( + dataset: "mbx-main", + language: "es" + ) + let loadDataExpectation = expectation(description: "Load Data") + _ = loadData(tilesetDescriptor: spanishTileset) { result in + switch result { + case .success(let region): + XCTAssert(region.id == self.regionId) + XCTAssert(region.completedResourceCount > 0) + XCTAssertEqual(region.requiredResourceCount, region.completedResourceCount) + case .failure(let error): + XCTFail("Unable to load Region, \(error.localizedDescription)") + } + loadDataExpectation.fulfill() + } + wait( + for: [loadDataExpectation, indexChangedExpectation], + timeout: 200, + enforceOrder: true + ) + + let offlineUpdateExpectation = delegate.offlineUpdateExpectation + searchEngine.search(query: "café") + wait(for: [offlineUpdateExpectation], timeout: 10) + + XCTAssertNil(delegate.error) + XCTAssertNil(delegate.error?.localizedDescription) + XCTAssertNotNil(searchEngine.responseInfo) + XCTAssertFalse(delegate.resolvedResults.isEmpty) + XCTAssertFalse(searchEngine.suggestions.isEmpty) + } + func testNoData() { clearData()