refactor(ios/engine): for legacy downloadLexicalModel API

This commit is contained in:
jahorton 2020-07-09 10:21:53 +07:00
parent c563bfac51
commit 269bc2eed5
7 changed files with 150 additions and 65 deletions

View file

@ -15,6 +15,7 @@ public enum APILexicalModelFetchError: Error {
case parsingError(Error)
}
@available(*, deprecated)
public class APILexicalModelRepository: LexicalModelRepository {
private let modelsAPIURL = URLComponents(string: "https://api.keyman.com/model")!

View file

@ -45,7 +45,12 @@ extension Queries {
private static let MODEL_ENDPOINT = URLComponents(string: "https://api.keyman.com/model")!
public static func fetch(forLanguageCode bcp47: String,
withSession session: URLSession = .shared,
fetchCompletion: @escaping JSONQueryCompletionBlock<[Result]>) {
fetch(forLanguageCode: bcp47, withSession: URLSession.shared, fetchCompletion: fetchCompletion)
}
internal static func fetch(forLanguageCode bcp47: String,
withSession session: URLSession,
fetchCompletion: @escaping JSONQueryCompletionBlock<[Result]>) {
// Step 1: build the query
var urlComponents = MODEL_ENDPOINT

View file

@ -30,6 +30,11 @@ extension Queries {
}
}
enum Entry {
case success(ResultEntry)
case failure(ResultError?)
}
struct Result: Decodable {
let keyboards: [String : ResultComponent]?
let models: [String : ResultComponent]?
@ -62,6 +67,21 @@ extension Queries {
let modelValueSet = try rootValues.nestedContainer(keyedBy: IDCodingKey.self, forKey: .models)
models = try modelValueSet.allKeys.reduce([String: ResultComponent](), dictionaryReducer(container: modelValueSet, category: "lexical model"))
}
func entryFor<FullID: LanguageResourceFullID>(_ fullID: FullID) -> Entry {
var result: ResultComponent?
if let fullID = fullID as? FullKeyboardID, let keyboards = self.keyboards {
result = keyboards[fullID.keyboardID]
} else if let fullID = fullID as? FullLexicalModelID, let models = self.models {
result = models[fullID.lexicalModelID]
}
if let entry = result as? ResultEntry {
return .success(entry)
} else {
return .failure(result as? ResultError)
}
}
}
private static let PACKAGE_VERSION_ENDPOINT = URLComponents(string: "https://api.keyman.com/package-version")!

View file

@ -10,7 +10,7 @@ import Foundation
// The core `Queries` definition provides common utility methods, handlers, etc across our API queries.
class Queries {
public enum FetchError: Error {
public enum FetchError: LocalizedError {
case networkError(Error)
case noData
case parsingError(Error)
@ -27,6 +27,17 @@ class Queries {
}
}
public enum ResultError: LocalizedError {
case unqueried
var localizedDescription: String {
switch self {
case .unqueried:
return "Query was not run against specified parameter"
}
}
}
public struct IDCodingKey: CodingKey {
/* Required by CodingKey, though we don't particularly need these */
public var intValue: Int?

View file

@ -15,6 +15,7 @@ import Foundation
// only accessible within the library.
public class ResourceDownloadManager {
// internal b/c testing access.
internal var session: URLSession
internal var downloader: ResourceDownloadQueue
private var isDidUpdateCheck = false
@ -25,11 +26,13 @@ public class ResourceDownloadManager {
public static let shared = ResourceDownloadManager()
internal init() {
session = URLSession.shared
downloader = ResourceDownloadQueue()
}
// Intended only for use in testing!
internal init(session: URLSession, autoExecute: Bool) {
self.session = session
downloader = ResourceDownloadQueue(session: session, autoExecute: autoExecute)
}
@ -220,24 +223,8 @@ public class ResourceDownloadManager {
}
// MARK - Lexical models
private func getInstallableLexicalModelMetadata(withID lexicalModelID: String, languageID: String) -> InstallableLexicalModel? {
// Grab info for the relevant API version of the keyboard.
guard let keyboard = Manager.shared.apiLexicalModelRepository.installableLexicalModel(withID: lexicalModelID, languageID: languageID)
else {
let message = "Lexical model not found with id: \(lexicalModelID), languageID: \(languageID)"
let error = NSError(domain: "Keyman", code: 0,
userInfo: [NSLocalizedDescriptionKey: message])
// Ideal - use a LexicalModelFullID instead. But, that's a API shift.
self.resourceDownloadFailed(for: [] as [InstallableLexicalModel], with: error)
return nil
}
return keyboard
}
// Can be called by the cloud keyboard downloader and utilized.
// Can be called by the keyboard downloader and utilized.
/// Starts the process of fetching the package file of the lexical model for the given language ID
/// first it fetches the list of lexical models for the given language
@ -282,56 +269,47 @@ public class ResourceDownloadManager {
isUpdate: Bool,
fetchRepositoryIfNeeded: Bool = true,
completionBlock: CompletionHandler<InstallableLexicalModel>? = nil) {
// TODO: We should always force a refetch after new keyboards are installed so we can redo our language queries.
// That should probably be done on successful keyboard installs, not here, though.
if fetchRepositoryIfNeeded {
// A temp measure to make sure things aren't totally broken. Definitely not optimal.
Manager.shared.apiLexicalModelRepository.fetch(completionHandler: nil)
}
guard let _ = Manager.shared.apiLexicalModelRepository.lexicalModels else {
if fetchRepositoryIfNeeded {
log.info("Fetching repository from API for lexicalModel download")
Manager.shared.apiLexicalModelRepository.fetch(completionHandler: fetchHandler(for: .lexicalModel) {
self.downloadLexicalModel(withID: lexicalModelID, languageID: languageID, isUpdate: isUpdate, fetchRepositoryIfNeeded: false, completionBlock: completionBlock)
})
return
} else {
let message = "Lexical model repository not yet fetched"
let error = NSError(domain: "Keyman", code: 0, userInfo: [NSLocalizedDescriptionKey: message])
self.resourceDownloadFailed(for: [] as [InstallableLexicalModel], with: error)
// Note: in this case, someone knows the "full ID" of the model already, but NOT its location.
// For lexical models, we can use either the LexicalModel query or the PackageVersion query.
// For consistency with keyboard download behavior, we use the PackageVersion query here.
let lmFullID = FullLexicalModelID(lexicalModelID: lexicalModelID, languageID: languageID)
Queries.PackageVersion.fetch(for: [lmFullID], withSession: session) { result, error in
guard let result = result, error == nil else {
log.info("Error occurred requesting location for \(lmFullID.description)")
self.resourceDownloadFailed(forFullID: lmFullID, with: error ?? .noData)
return
}
}
// Grab info for the relevant API version of the keyboard.
guard let lexicalModel = getInstallableLexicalModelMetadata(withID: lexicalModelID, languageID: languageID),
let filename = Manager.shared.apiLexicalModelRepository.lexicalModels?[lexicalModelID]?.packageFilename
else {
return
}
// Perform common 'can download' check. We need positive reachability and no prior download queue.
let queueState = downloader.state
guard queueState == .clear else {
resourceDownloadFailed(for: [lexicalModel], with: queueState.error ?? NSError(domain: "Keyman", code: 0, userInfo: [NSLocalizedDescriptionKey: "Already busy downloading something"]))
return
}
let batch = self.buildPackageBatch(forFullID: FullLexicalModelID(lexicalModelID: lexicalModelID, languageID: languageID),
from: URL.init(string: filename)!,
withNotifications: !isUpdate,
withResource: lexicalModel,
completionBlock: completionBlock ?? { package, error in
// If the caller doesn't specify a completion block, this will carry out a default installation.
if let package = package {
try? ResourceFileManager.shared.install(resourceWithID: FullLexicalModelID(lexicalModelID: lexicalModelID, languageID: languageID), from: package)
guard case let .success(data) = result.entryFor(lmFullID) else {
if case let .failure(errorEntry) = result.entryFor(lmFullID) {
if let errorEntry = errorEntry {
log.info("Query reported error: \(String(describing: errorEntry.error))")
}
}
self.resourceDownloadFailed(forFullID: lmFullID, with: Queries.ResultError.unqueried)
return
}
// else error: already handled by wrapping closure set within buildPackageBatch.
})
downloader.queue(.simpleBatch(batch))
// Perform common 'can download' check. We need positive reachability and no prior download queue.
guard self.downloader.state == .clear else {
self.resourceDownloadFailed(forFullID: lmFullID, with: self.downloader.state.error ??
NSError(domain: "Keyman", code: 0, userInfo: [NSLocalizedDescriptionKey: "Already busy downloading something"]))
return
}
let completionClosure: CompletionHandler<InstallableLexicalModel> = completionBlock ?? { package, error in
// If the caller doesn't specify a completion block, this will carry out a default installation.
if let package = package {
try? ResourceFileManager.shared.install(resourceWithID: FullLexicalModelID(lexicalModelID: lexicalModelID, languageID: languageID), from: package)
}
}
self.downloadPackage(forFullID: lmFullID,
from: URL.init(string: data.packageURL)!,
withNotifications: !isUpdate,
completionBlock: completionClosure)
}
}
@ -743,4 +721,18 @@ public class ResourceDownloadManager {
value: notification)
}
}
internal func resourceDownloadFailed<FullID: LanguageResourceFullID>(forFullID fullID: FullID, with error: Error) {
if let _ = fullID as? FullKeyboardID {
let notification = KeyboardDownloadFailedNotification(keyboards: [], error: error)
NotificationCenter.default.post(name: Notifications.keyboardDownloadFailed,
object: self,
value: notification)
} else if let _ = fullID as? FullLexicalModelID {
let notification = LexicalModelDownloadFailedNotification(lmOrLanguageID: fullID.languageID, error: error)
NotificationCenter.default.post(name: Notifications.lexicalModelDownloadFailed,
object: self,
value: notification)
}
}
}

View file

@ -42,10 +42,13 @@ class QueryPackageVersionTests: XCTestCase {
TestUtils.LexicalModels.mtnt.fullID,
badLexFullID]
let expectation = XCTestExpectation(description: "Query complete and results analyzed")
// As it's a mocked fetch, it happens synchronously.
Queries.PackageVersion.fetch(for: fullIDs, withSession: mockedURLSession!) { results, error in
if let _ = error {
XCTFail(String(describing: error))
expectation.fulfill()
return
}
XCTAssertNotNil(results)
@ -94,6 +97,60 @@ class QueryPackageVersionTests: XCTestCase {
}
}
}
expectation.fulfill()
}
wait(for: [expectation], timeout: 5)
}
func testResultEntryFor() throws {
let mockedResult = TestUtils.Downloading.MockResult(location: TestUtils.Queries.package_version_case_1, error: nil)
mockedURLSession?.queueMockResult(.data(mockedResult))
let badKbdFullID = FullKeyboardID(keyboardID: "foo", languageID: "en")
let badLexFullID = FullLexicalModelID(lexicalModelID: "bar", languageID: "km")
let fullIDs = [TestUtils.Keyboards.khmer_angkor.fullID,
TestUtils.Keyboards.sil_euro_latin.fullID,
badKbdFullID,
TestUtils.LexicalModels.mtnt.fullID,
badLexFullID]
let expectation = XCTestExpectation(description: "Query complete and results analyzed")
// As it's a mocked fetch, it happens synchronously.
Queries.PackageVersion.fetch(for: fullIDs, withSession: mockedURLSession!) { results, error in
if let _ = error {
XCTFail(String(describing: error))
expectation.fulfill()
return
}
XCTAssertNotNil(results)
if let results = results {
XCTAssertNotNil(results.keyboards)
XCTAssertNotNil(results.models)
let khmer_angkor = results.entryFor(TestUtils.Keyboards.khmer_angkor.fullID)
if case .failure(_) = khmer_angkor {
XCTFail("API result object reported error for khmer_angkor, not a version entry")
}
let foo = results.entryFor(badKbdFullID)
if case .success(_) = foo {
XCTFail("API result object reported a version entry for foo, not an error")
}
let foobar = results.entryFor(FullKeyboardID(keyboardID: "foobar", languageID: "en"))
if case .success(_) = foobar {
XCTFail("Query should not have results data for unqueried resource")
}
}
expectation.fulfill()
}
wait(for: [expectation], timeout: 5)
}
}

View file

@ -133,7 +133,6 @@ class MainViewController: UIViewController, TextViewDelegate, UIActionSheetDeleg
// Pre-load for use in update checks.
Manager.shared.apiKeyboardRepository.fetch()
Manager.shared.apiLexicalModelRepository.fetch()
// Implement a default color...
var bgColor = UIColor(red: 1.0, green: 1.0, blue: 207.0 / 255.0, alpha: 1.0)