mirror of
https://github.com/keymanapp/keyman.git
synced 2026-10-18 11:57:38 +00:00
Merge pull request #3207 from keymanapp/refactor/ios/engine/selective-resource-install-abstraction
refactor(ios): Selective resource installation via abstraction
This commit is contained in:
commit
acaa3fae16
8 changed files with 192 additions and 102 deletions
|
|
@ -11,7 +11,7 @@ import Foundation
|
|||
public class KeyboardKeymanPackage : TypedKeymanPackage<InstallableKeyboard> {
|
||||
internal var keyboards: [KMPKeyboard]!
|
||||
|
||||
override init(metadata: KMPMetadata, folder: URL) {
|
||||
override internal init(metadata: KMPMetadata, folder: URL) {
|
||||
super.init(metadata: metadata, folder: folder)
|
||||
self.keyboards = []
|
||||
|
||||
|
|
|
|||
|
|
@ -15,14 +15,18 @@ public enum KMPError : String, Error {
|
|||
case fileSystem = "Unable to create directory structure in file system."
|
||||
case copyFiles = "Unable to copy keyboard files to file system."
|
||||
case wrongPackageType = "Package provided does not match the type expected by this method"
|
||||
case resourceNotInPackage = "Resource cannot be found in specified package"
|
||||
}
|
||||
|
||||
/**
|
||||
* Common base class for the different KeymanPackage types.
|
||||
*/
|
||||
public class KeymanPackage {
|
||||
static private let kmpFile = "kmp.json"
|
||||
public let sourceFolder: URL
|
||||
internal let metadata: KMPMetadata
|
||||
|
||||
init(metadata: KMPMetadata, folder: URL) {
|
||||
internal init(metadata: KMPMetadata, folder: URL) {
|
||||
sourceFolder = folder
|
||||
self.metadata = metadata
|
||||
}
|
||||
|
|
@ -31,6 +35,21 @@ public class KeymanPackage {
|
|||
return metadata.packageType == .Keyboard
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the type of LanguageResource contained within the parsed Package.
|
||||
*/
|
||||
public func resourceType() -> LanguageResourceType {
|
||||
switch metadata.packageType {
|
||||
case .Keyboard:
|
||||
return .keyboard
|
||||
case .LexicalModel:
|
||||
return .lexicalModel
|
||||
default:
|
||||
// See KeymanPackage.parse below; an error is thrown there for case .Unsupported.
|
||||
fatalError("KeymanPackage.parse failed to block construction of unsupported KeymanPackage instance")
|
||||
}
|
||||
}
|
||||
|
||||
// to be overridden by subclasses
|
||||
public func defaultInfoHtml() -> String {
|
||||
return "base class!"
|
||||
|
|
@ -67,7 +86,13 @@ public class KeymanPackage {
|
|||
public var installableResourceSets: [[AnyLanguageResource]] {
|
||||
fatalError("abstract base method went unimplemented by derived class")
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Parses a decompressed KMP's metadata file, producing the typed KeymanPackage instance corresponding to it.
|
||||
*
|
||||
* Typecast the return value to either KeyboardKeymanPackage or LexicalModelKeymanPackage for richly-typed
|
||||
* information about the package's contents.
|
||||
*/
|
||||
static public func parse(_ folder: URL) -> KeymanPackage? {
|
||||
do {
|
||||
var path = folder
|
||||
|
|
@ -133,4 +158,20 @@ public class TypedKeymanPackage<TypedLanguageResource: LanguageResource>: Keyman
|
|||
public override var installableResourceSets: [[AnyLanguageResource]] {
|
||||
return installables
|
||||
}
|
||||
|
||||
public func findResource(withID fullID: TypedLanguageResource.FullID) -> TypedLanguageResource? {
|
||||
let matchesFound: [TypedLanguageResource] = self.installables.compactMap { set in
|
||||
let setMatches: [TypedLanguageResource] = set.compactMap { other in
|
||||
if fullID.id == other.id && fullID.languageID == other.languageID {
|
||||
return other
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return setMatches.count > 0 ? setMatches[0] : nil
|
||||
}
|
||||
|
||||
return matchesFound.count > 0 ? matchesFound[0] : nil
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import Foundation
|
|||
public class LexicalModelKeymanPackage : TypedKeymanPackage<InstallableLexicalModel> {
|
||||
internal var models : [KMPLexicalModel]!
|
||||
|
||||
override init(metadata: KMPMetadata, folder: URL) {
|
||||
override internal init(metadata: KMPMetadata, folder: URL) {
|
||||
super.init(metadata: metadata, folder: folder)
|
||||
self.models = []
|
||||
|
||||
|
|
|
|||
|
|
@ -617,60 +617,11 @@ public class Manager: NSObject, UIGestureRecognizerDelegate {
|
|||
throw KMPError.wrongPackageType
|
||||
}
|
||||
|
||||
for r in kmp.resources as! [KMPKeyboard] {
|
||||
let installableFiles: [(InstallableKeyboard, [(String, URL)])] = r.typedInstallableResources.map { resource in
|
||||
// (source file, destination file)
|
||||
var set: [(String, URL)] = [(resource.sourceFilename, Storage.active.resourceURL(for: resource)!)]
|
||||
let installableFonts: [(String, URL)] = resource.fonts.map { font in
|
||||
// KMPs only list a single font file for each entry whenever one is included.
|
||||
// A pre-existing assumption.
|
||||
let fontFile = font.source[0]
|
||||
return (fontFile, Storage.active.fontURL(forResource: resource, filename: fontFile)!)
|
||||
}
|
||||
|
||||
set.append(contentsOf: installableFonts)
|
||||
|
||||
// (resource, source-destination file mapping)
|
||||
return (resource, set)
|
||||
}
|
||||
|
||||
var haveInstalledOne = false
|
||||
for set in installableFiles {
|
||||
let resource = set.0
|
||||
let files = set.1
|
||||
|
||||
do {
|
||||
try FileManager.default.createDirectory(at: Storage.active.resourceDir(for: resource)!,
|
||||
withIntermediateDirectories: true)
|
||||
} catch {
|
||||
log.error("Could not create dir for download: \(error)")
|
||||
throw KMPError.fileSystem
|
||||
}
|
||||
|
||||
do {
|
||||
for item in files {
|
||||
var filePath = folder
|
||||
filePath.appendPathComponent(item.0)
|
||||
|
||||
// TODO: The rest of this block may be replaced with ResourceFileManager.copyWithOverwrite
|
||||
// once this method is fully abstracted and its core is placed within said class.
|
||||
if(FileManager.default.fileExists(atPath: (item.1).path)) {
|
||||
try FileManager.default.removeItem(at: item.1)
|
||||
}
|
||||
try FileManager.default.copyItem(at: filePath, to: item.1)
|
||||
|
||||
}
|
||||
} catch {
|
||||
log.error("Error saving the download: \(error)")
|
||||
throw KMPError.copyFiles
|
||||
}
|
||||
|
||||
// Only one keyboard is installed by default from a KMP.
|
||||
// Also, only the first language-pairing it contains
|
||||
if !haveInstalledOne {
|
||||
Manager.shared.addKeyboard(resource)
|
||||
haveInstalledOne = true
|
||||
}
|
||||
for resourceSet in kmp.installables {
|
||||
for resource in resourceSet {
|
||||
try ResourceFileManager.shared.install(resourceWithID: resource.fullID, from: kmp)
|
||||
// Install the keyboard for only the first language pairing defined in the package.
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -681,48 +632,9 @@ public class Manager: NSObject, UIGestureRecognizerDelegate {
|
|||
throw KMPError.wrongPackageType
|
||||
}
|
||||
|
||||
for r in kmp.resources as! [KMPLexicalModel] {
|
||||
let installableFiles: [(InstallableLexicalModel, [(String, URL)])] = r.installableResources.map { resource in
|
||||
// (source file, destination file)
|
||||
let set: [(String, URL)] = [(resource.sourceFilename, Storage.active.resourceURL(for: resource)!)]
|
||||
// no fonts for lexical models
|
||||
|
||||
// (resource, source-destination file mapping)
|
||||
return (resource, set)
|
||||
}
|
||||
|
||||
for set in installableFiles {
|
||||
let resource = set.0
|
||||
let files = set.1
|
||||
|
||||
do {
|
||||
try FileManager.default.createDirectory(at: Storage.active.resourceDir(for: resource)!,
|
||||
withIntermediateDirectories: true)
|
||||
} catch {
|
||||
log.error("Could not create dir for download: \(error)")
|
||||
throw KMPError.fileSystem
|
||||
}
|
||||
|
||||
do {
|
||||
for item in files {
|
||||
var filePath = folder
|
||||
filePath.appendPathComponent(item.0)
|
||||
|
||||
// TODO: The rest of this block may be replaced with ResourceFileManager.copyWithOverwrite
|
||||
// once this method is fully abstracted and its core is placed within said class.
|
||||
if(FileManager.default.fileExists(atPath: (item.1).path)) {
|
||||
try FileManager.default.removeItem(at: item.1)
|
||||
}
|
||||
try FileManager.default.copyItem(at: filePath, to: item.1)
|
||||
}
|
||||
} catch {
|
||||
log.error("Error saving the lexical model download: \(error)")
|
||||
throw KMPError.copyFiles
|
||||
}
|
||||
|
||||
// All pairings are installed for lexical models.
|
||||
// Also, all models within the KMP
|
||||
Manager.addLexicalModel(resource)
|
||||
try kmp.installables.forEach { resourceSet in
|
||||
try resourceSet.forEach { resource in
|
||||
try ResourceFileManager.shared.install(resourceWithID: resource.fullID, from: kmp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -178,4 +178,54 @@ public class ResourceFileManager {
|
|||
completionHandler(error)
|
||||
}
|
||||
}
|
||||
|
||||
public func install<ResourceType: LanguageResource,
|
||||
PackageType: TypedKeymanPackage<ResourceType>> (
|
||||
resourceWithID fullID: ResourceType.FullID,
|
||||
from package: PackageType) throws {
|
||||
|
||||
guard let resource = package.findResource(withID: fullID) else {
|
||||
throw KMPError.resourceNotInPackage
|
||||
}
|
||||
|
||||
// (source, destination)
|
||||
var installableFiles: [(String, URL)] = [(resource.sourceFilename, Storage.active.resourceURL(for: resource)!)]
|
||||
let installableFonts: [(String, URL)] = resource.fonts.map { font in
|
||||
// KMPs only list a single font file for each entry whenever one is included.
|
||||
// A pre-existing assumption.
|
||||
let fontFile = font.source[0]
|
||||
return (fontFile, Storage.active.fontURL(forResource: resource, filename: fontFile)!)
|
||||
}
|
||||
|
||||
installableFiles.append(contentsOf: installableFonts)
|
||||
|
||||
do {
|
||||
try FileManager.default.createDirectory(at: Storage.active.resourceDir(for: resource)!,
|
||||
withIntermediateDirectories: true)
|
||||
} catch {
|
||||
log.error("Could not create installation directory: \(error)")
|
||||
throw KMPError.fileSystem
|
||||
}
|
||||
|
||||
do {
|
||||
for item in installableFiles {
|
||||
var filePath = package.sourceFolder
|
||||
filePath.appendPathComponent(item.0)
|
||||
try copyWithOverwrite(from: filePath, to: item.1)
|
||||
}
|
||||
} catch {
|
||||
log.error("Error installing the resource: \(error)")
|
||||
throw KMPError.copyFiles
|
||||
}
|
||||
|
||||
// There's no generalized method for this quite yet. Manager doesn't need even
|
||||
// more of these.
|
||||
if let keyboard = resource as? InstallableKeyboard {
|
||||
Manager.shared.addKeyboard(keyboard)
|
||||
} else if let lexicalModel = resource as? InstallableLexicalModel {
|
||||
Manager.addLexicalModel(lexicalModel)
|
||||
} else {
|
||||
fatalError("Cannot install instance of unexpected LanguageResource subclass")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,4 +69,65 @@ class FileManagementTests: XCTestCase {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testInstallKeyboardFromPackage() throws {
|
||||
// Standard installation
|
||||
ResourceFileManager.shared.prepareKMPInstall(from: TestUtils.Keyboards.khmerAngkorKMP) { kmp, error in
|
||||
XCTAssertNotNil(kmp, "Failed to prepare KMP for installation")
|
||||
XCTAssertNil(error, "Error occurred while preparing KMP for installation")
|
||||
guard let kmp = kmp as? KeyboardKeymanPackage else {
|
||||
XCTFail("KMP resource type improperly recognized - expected a keyboard package!")
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
try ResourceFileManager.shared.install(resourceWithID: TestUtils.Keyboards.khmer_angkor.fullID, from: kmp)
|
||||
} catch {
|
||||
XCTFail("Unexpected error during KeyboardPackage install")
|
||||
}
|
||||
|
||||
let installURL = Storage.active.keyboardURL(forID: "khmer_angkor", version: "1.0.6")
|
||||
|
||||
XCTAssertTrue(FileManager.default.fileExists(atPath: installURL.path),
|
||||
"Could not find installed keyboard file")
|
||||
|
||||
let keyboards = Storage.active.userDefaults.userKeyboards!
|
||||
|
||||
XCTAssertEqual(keyboards.count, 1, "Unexpected number of keyboards were installed")
|
||||
XCTAssertEqual(keyboards[0].id, "khmer_angkor", "Installed keyboard ID mismatch")
|
||||
|
||||
// While the LanguageResource definition we provided lacks font definitions, the
|
||||
// KMP's definition has that data. By default, the KMP's definition takes precedence.
|
||||
let fontURL = Storage.active.fontURL(forKeyboardID: "khmer_angkor", filename: "Mondulkiri-R.ttf")
|
||||
XCTAssertTrue(FileManager.default.fileExists(atPath: fontURL.path))
|
||||
}
|
||||
}
|
||||
|
||||
func testInstallLexicalModelFromPackage() {
|
||||
ResourceFileManager.shared.prepareKMPInstall(from: TestUtils.LexicalModels.mtntKMP) { kmp, error in
|
||||
XCTAssertNotNil(kmp, "Failed to prepare KMP for installation")
|
||||
XCTAssertNil(error, "Error occurred while preparing KMP for installation")
|
||||
guard let kmp = kmp as? LexicalModelKeymanPackage else {
|
||||
XCTFail("KMP resource type improperly recognized - expected a lexical model package!")
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
try ResourceFileManager.shared.install(resourceWithID: TestUtils.LexicalModels.mtnt.fullID, from: kmp)
|
||||
} catch {
|
||||
XCTFail("Unexpected error during LexicalModelPackage install")
|
||||
}
|
||||
|
||||
let installURL = Storage.active.lexicalModelURL(forID: "nrc.en.mtnt", version: "0.1.4")
|
||||
|
||||
XCTAssertTrue(FileManager.default.fileExists(atPath: installURL.path),
|
||||
"Could not find installed lexical model file")
|
||||
|
||||
let models = Storage.active.userDefaults.userLexicalModels!
|
||||
|
||||
// This variant is selective - only a single pairing should be installed for the model.
|
||||
XCTAssertEqual(models.count, 1, "Unexpected number of models were installed")
|
||||
XCTAssertEqual(models[0].id, "nrc.en.mtnt", "Installed lexical model ID mismatch")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -74,4 +74,30 @@ class KeymanPackageTests: XCTestCase {
|
|||
XCTFail("KeymanPackage.extract failed with error \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
func testPackageFindResourceMatch() {
|
||||
ResourceFileManager.shared.prepareKMPInstall(from: TestUtils.Keyboards.khmerAngkorKMP) { kmp, _ in
|
||||
guard let kmp = kmp as? KeyboardKeymanPackage else {
|
||||
XCTFail("Incorrect package type loaded for test")
|
||||
return
|
||||
}
|
||||
XCTAssertNotNil(kmp.findResource(withID: TestUtils.Keyboards.khmer_angkor.fullID))
|
||||
// This keyboard's not in the specified testing package.
|
||||
XCTAssertNil(kmp.findResource(withID: TestUtils.Keyboards.khmer10.fullID))
|
||||
|
||||
// Thanks to our package typing hierarchy, it's impossible to even TRY finding
|
||||
// a FullLexicalModelID within a KeyboardKeymanPackage!
|
||||
}
|
||||
|
||||
ResourceFileManager.shared.prepareKMPInstall(from: TestUtils.LexicalModels.mtntKMP) { kmp, _ in
|
||||
guard let kmp = kmp as? LexicalModelKeymanPackage else {
|
||||
XCTFail("Incorrect package type loaded for test")
|
||||
return
|
||||
}
|
||||
XCTAssertNotNil(kmp.findResource(withID: TestUtils.LexicalModels.mtnt.fullID))
|
||||
|
||||
// Thanks to our package typing hierarchy, it's impossible to even TRY finding
|
||||
// a FullKeyboardID within a LexicalModelKeymanPackage!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ extension TestUtils {
|
|||
static let khmerAngkorKMP = TestUtils.keyboardsBundle.url(forResource: "khmer_angkor", withExtension: "kmp")!
|
||||
static let khmer_angkor = InstallableKeyboard(id: "khmer_angkor",
|
||||
name: "Khmer Angkor",
|
||||
languageID: "kh",
|
||||
languageID: "km",
|
||||
languageName: "Khmer (Central)",
|
||||
version: "1.0.6",
|
||||
isRTL: false,
|
||||
|
|
@ -23,7 +23,7 @@ extension TestUtils {
|
|||
isCustom: false)
|
||||
static let khmer10 = InstallableKeyboard(id: "khmer10",
|
||||
name: "Khmer (NiDA)",
|
||||
languageID: "kh",
|
||||
languageID: "km",
|
||||
languageName: "Khmer (Central)",
|
||||
version: "1.3",
|
||||
isRTL: false,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue