mirror of
https://github.com/keymanapp/keyman.git
synced 2026-08-20 23:37:41 +00:00
For Keyman for iOS, add the new `keyman-version` parameter to the api.keyman.com/package-version call so that updates to packages that are not supported on the current version of Keyman will not be offered. This supports the scenario where an updated keyboard or lexical model depends on a newer version of Keyman. Note that an older version of the keyboard or lexical model package will not be offered (the user can still download and install an older version manually, but generally, the recommended solution is to upgrade Keyman; there would be significant cost to add support for querying and installation of older version keyboards on the server side, for limited benefit). Also change order of handling for response so that error responses are recognized even if non-error fields are present. (The current api endpoint, before keymanapp/api.keyman.com#325 lands, can return `kmp` and `version` fields even when an `error` field was present, and the iOS code would see that as a valid response for upgrade, when it shouldn't. The keymanapp/api.keyman.com#325 change ensures that the additional fields are not set if there is an `error` field.) Relates-to: keymanapp/api.keyman.com#325
135 lines
4.5 KiB
Swift
135 lines
4.5 KiB
Swift
//
|
|
// Queries+PackageVersion.swift
|
|
// KeymanEngine
|
|
//
|
|
// Created by Joshua Horton on 6/25/20.
|
|
// Copyright © 2020 SIL International. All rights reserved.
|
|
//
|
|
|
|
import Foundation
|
|
import os.log
|
|
|
|
extension Queries {
|
|
// Kept internal; the query is 'publicly exposed' through KeymanPackage and ResourceFileManager.
|
|
class PackageVersion {
|
|
class ResultComponent {}
|
|
|
|
class ResultEntry: ResultComponent, Decodable {
|
|
let version: String
|
|
let packageURL: String
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case version
|
|
case packageURL = "kmp"
|
|
}
|
|
}
|
|
|
|
class ResultError: ResultComponent, Decodable {
|
|
let error: String
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case error
|
|
}
|
|
}
|
|
|
|
enum Entry {
|
|
case success(ResultEntry)
|
|
case failure(ResultError?)
|
|
}
|
|
|
|
struct Result: Decodable {
|
|
let keyboards: [String : ResultComponent]?
|
|
let models: [String : ResultComponent]?
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case keyboards
|
|
case models
|
|
}
|
|
|
|
init(from decoder: Decoder) throws {
|
|
let rootValues = try decoder.container(keyedBy: CodingKeys.self)
|
|
|
|
func dictionaryReducer(container: KeyedDecodingContainer<Queries.IDCodingKey>, category: String) -> ([String: ResultComponent], Queries.IDCodingKey) throws -> [String: ResultComponent] {
|
|
return { (dict, id) -> [String: ResultComponent] in
|
|
var dict = dict
|
|
if let error = try? container.decode(ResultError.self, forKey: id) {
|
|
dict[id.stringValue] = error
|
|
} else if let entry = try? container.decode(ResultEntry.self, forKey: id) {
|
|
dict[id.stringValue] = entry
|
|
} else {
|
|
throw FetchError.decodingError(category, id.stringValue)
|
|
}
|
|
return dict
|
|
}
|
|
}
|
|
|
|
if rootValues.contains(.keyboards) {
|
|
let keyboardValueSet = try rootValues.nestedContainer(keyedBy: IDCodingKey.self, forKey: .keyboards)
|
|
keyboards = try keyboardValueSet.allKeys.reduce([String: ResultComponent](), dictionaryReducer(container: keyboardValueSet, category: "keyboard"))
|
|
} else {
|
|
keyboards = nil
|
|
}
|
|
|
|
if rootValues.contains(.models) {
|
|
let modelValueSet = try rootValues.nestedContainer(keyedBy: IDCodingKey.self, forKey: .models)
|
|
models = try modelValueSet.allKeys.reduce([String: ResultComponent](), dictionaryReducer(container: modelValueSet, category: "lexical model"))
|
|
} else {
|
|
models = nil
|
|
}
|
|
}
|
|
|
|
func entryFor(_ key: KeymanPackage.Key) -> Entry {
|
|
var result: ResultComponent?
|
|
|
|
switch(key.type) {
|
|
case .keyboard:
|
|
result = keyboards?[key.id]
|
|
case .lexicalModel:
|
|
result = models?[key.id]
|
|
}
|
|
|
|
if let entry = result as? ResultEntry {
|
|
return .success(entry)
|
|
} else {
|
|
return .failure(result as? ResultError)
|
|
}
|
|
}
|
|
}
|
|
|
|
private static let PACKAGE_VERSION_ENDPOINT = URLComponents(string: "\(KeymanHosts.API_KEYMAN_COM)/package-version")!
|
|
|
|
public static func fetch(for packageKeys: [KeymanPackage.Key],
|
|
withSession session: URLSession = .shared,
|
|
fetchCompletion: @escaping JSONQueryCompletionBlock<Result>) {
|
|
// Step 1: build the query
|
|
var urlComponents = PACKAGE_VERSION_ENDPOINT
|
|
|
|
let queryItems: [URLQueryItem] = packageKeys.map { key in
|
|
var resourceField: String
|
|
switch(key.type) {
|
|
case .keyboard:
|
|
resourceField = "keyboard"
|
|
case .lexicalModel:
|
|
resourceField = "model"
|
|
}
|
|
|
|
return URLQueryItem(name: resourceField, value: key.id)
|
|
}
|
|
|
|
urlComponents.queryItems = queryItems + [
|
|
URLQueryItem(name: "platform", value: "ios"),
|
|
URLQueryItem(name: "keyman-version", value: Version.current.plainString)
|
|
]
|
|
let message = "Querying package versions through API endpoint: \(urlComponents.url!)"
|
|
os_log("%{public}s", log:KeymanEngineLogger.resources, type: .info, message)
|
|
SentryManager.breadcrumb(message)
|
|
|
|
// Step 2: configure the completion closure.
|
|
let completionClosure = Queries.jsonDataTaskCompletionAdapter(resultType: Result.self, completionBlock: fetchCompletion)
|
|
|
|
// Step 3: run the actual query, letting the prepared completion closure take care of the rest.
|
|
let task = session.dataTask(with: urlComponents.url!, completionHandler: completionClosure)
|
|
task.resume()
|
|
}
|
|
}
|
|
}
|