chore(ios/engine): package support-state querying

This commit is contained in:
jahorton 2020-07-10 13:53:19 +07:00
commit 84ee2a574a
3 changed files with 165 additions and 29 deletions

View file

@ -54,6 +54,63 @@ public class KeymanPackage {
}
}
/**
* Indicates the distribution level and state of the package.
*/
public enum SupportState: String, Codable {
/**
* Indicates that the support level for the package is unknown. Generally occurs when a
* package is installed via file-sharing before KeymanEngine is able to perform a package-version check.
*/
case unknown
/**
* Indicates that the package is publicly distributed but no longer maintained. Indicates that other packages targetting
* the same targets are more favored.
*/
case deprecated
/**
* Indicates that this package receives full support and maintenance from the resource development community at large.
*/
case publiclyReleased = "publicly released"
/**
* Indicates that this package is known to not be publicly distributed.
*/
case custom
}
/**
* Cloud/query related metadata not tracked (or even trackable) within kmp.json regarding the support state of a keyboard.
*/
public struct SupportStateMetadata: Codable {
var latestVersion: String?
var timestampForLastQuery: TimeInterval?
var supportState: SupportState
init(from queryResult: Queries.PackageVersion.ResultComponent) {
if let entry = queryResult as? Queries.PackageVersion.ResultEntry {
self.latestVersion = entry.version
self.supportState = .publiclyReleased
} else /* if queryResult is Queries.PackageVersion.ResultError */ {
self.latestVersion = nil
// The package-version query knows nothing about it - must be custom.
self.supportState = .custom
}
self.timestampForLastQuery = NSDate().timeIntervalSince1970
}
init(fromQuery: Bool = true) {
latestVersion = nil
timestampForLastQuery = nil
supportState = fromQuery ? .publiclyReleased : .unknown
}
}
static private let kmpFile = "kmp.json"
public let sourceFolder: URL
public let id: String
@ -225,6 +282,63 @@ public class KeymanPackage {
return nil
}
}
/**
* Runs the package-version query for the specified packages to determine their current support-state.
*/
public static func querySupportStates(for keys: [Key], withSession session: URLSession = URLSession.shared, completionBlock: (([Key : SupportStateMetadata]?, Error?)-> Void)? = nil) {
Queries.PackageVersion.fetch(for: keys, withSession: session) { results, error in
guard error == nil, let results = results else {
completionBlock?(nil, error)
return
}
var keyboardStates: [Key : SupportStateMetadata] = [:]
keyboardStates.reserveCapacity(results.keyboards?.count ?? 0)
// Sadly, Dictionaries do not support mapping to other dictionaries when considering the keys.
results.keyboards?.forEach { key, value in
keyboardStates[KeymanPackage.Key(id: key, type: .keyboard)] = KeymanPackage.SupportStateMetadata(from: value)
}
var lexicalModelStates: [Key : SupportStateMetadata] = [:]
lexicalModelStates.reserveCapacity(results.models?.count ?? 0)
results.models?.forEach { key, value in
lexicalModelStates[KeymanPackage.Key(id: key, type: .lexicalModel)] = KeymanPackage.SupportStateMetadata(from: value)
}
// TODO: Save these states to UserDefaults!
// There will be no 'merge' conflicts, so we ignore them by simply selecting the original.
if let completionBlock = completionBlock {
let stateSet = keyboardStates.merging(lexicalModelStates, uniquingKeysWith: { lhs, _ in return lhs })
completionBlock(stateSet, nil)
}
}
}
/**
* Runs the package-version query for the specified packages to determine if any updates are available.
*/
public static func queryCurrentVersions(for keys: [Key], withSession session: URLSession = URLSession.shared, completionBlock: (([Key : Version]?, Error?) -> Void)? = nil) {
querySupportStates(for: keys, withSession: session) { stateSet, error in
guard error == nil, let stateSet = stateSet else {
completionBlock?(nil, error)
return
}
let versionSet: [Key: Version] = stateSet.compactMapValues { value in
if let versionString = value.latestVersion, let version = Version(versionString) {
return version
} else {
return nil
}
}
completionBlock?(versionSet, nil)
}
}
}
/**

View file

@ -399,35 +399,6 @@ public class ResourceDownloadManager {
}
}
/**
* Runs the package-version query against all installed resources to determine if any updates are available.
*/
public func fetchAvailableUpdates(completionBlock: (([KeymanPackage.Key]?, Error?) -> Void)? = nil) {
let userDefaults = Storage.active.userDefaults
let keyboardPackages = userDefaults.userKeyboards?.map { $0.packageKey }
let lexicalModelPackages = userDefaults.userLexicalModels?.map { $0.packageKey }
let packageKeys = (keyboardPackages ?? []) + (lexicalModelPackages ?? [])
Queries.PackageVersion.fetch(for: packageKeys) { results, error in
guard error == nil else {
completionBlock?(nil, error)
return
}
// If no completionBlock was specified, the caller simply wanted a prefetch.
// Any further processing we might try to do would go to waste, so stop here.
guard let completionBlock = completionBlock else {
return
}
// Check for updates among the returned versions IF a completion block is specified.
// This facilitates a more proactive update notification.
// TODO: flesh out!
}
}
public func getAvailableUpdates() -> [AnyLanguageResource]? {
// Relies upon KMManager's preload; this was the case before the rework.
if Manager.shared.apiKeyboardRepository.languages == nil && Manager.shared.apiLexicalModelRepository.languages == nil {

View file

@ -137,4 +137,55 @@ class KeymanPackageTests: XCTestCase {
// Deinit should have triggered - were the files automatically cleaned up?
XCTAssertFalse(FileManager.default.fileExists(atPath: tempDir.path))
}
// Analogous to QueryPackageVersionTests.testMockedBatchFetchParse, but with more analysis applied
// and more integration.
func testQueryCurrentVersions() throws {
let mockedURLSession = TestUtils.Downloading.URLSessionMock()
let expectation = XCTestExpectation(description: "The query completes as expected.")
// Test setup
let mockedResult = TestUtils.Downloading.MockResult(location: TestUtils.Queries.package_version_case_1, error: nil)
mockedURLSession.queueMockResult(.data(mockedResult))
let badKbdKey = KeymanPackage.Key(id: "foo", type: .keyboard)
let badLexKey = KeymanPackage.Key(id: "bar", type: .lexicalModel)
let packageKeys = [KeymanPackage.Key(forResource: TestUtils.Keyboards.khmer_angkor),
KeymanPackage.Key(forResource: TestUtils.Keyboards.sil_euro_latin),
KeymanPackage.Key(id: "foo", type: .keyboard),
KeymanPackage.Key(forResource: TestUtils.LexicalModels.mtnt),
KeymanPackage.Key(id: "bar", type: .lexicalModel)]
KeymanPackage.queryCurrentVersions(for: packageKeys, withSession: mockedURLSession) { results, error in
guard error == nil, let results = results else {
XCTFail()
expectation.fulfill()
return
}
let khmer_angkor = KeymanPackage.Key(forResource: TestUtils.Keyboards.khmer_angkor)
let sil_euro_latin = KeymanPackage.Key(forResource: TestUtils.Keyboards.sil_euro_latin)
let mtnt = KeymanPackage.Key(forResource: TestUtils.LexicalModels.mtnt)
XCTAssertEqual(results[khmer_angkor], Version("1.0.6"))
XCTAssertEqual(results[sil_euro_latin], Version("1.9.1"))
XCTAssertEqual(results[mtnt], Version("0.1.4"))
XCTAssertNil(results[badKbdKey])
XCTAssertNil(results[badLexKey])
expectation.fulfill()
}
wait(for: [expectation], timeout: 5)
// Post-execution cleanup
let queueWasCleared = mockedURLSession.queueIsEmpty
Queries.PackageVersion.resetCache()
if !queueWasCleared {
throw NSError(domain: "Keyman",
code: 4,
userInfo: [NSLocalizedDescriptionKey: "A test did not fully utilize its queued mock results!"])
}
}
}