From 65a82a1509a96e64e5788f448d66207d0d7222fb Mon Sep 17 00:00:00 2001 From: jahorton Date: Tue, 2 Jun 2020 10:27:52 +0700 Subject: [PATCH 01/11] change(ios/engine): code organization - relocates *KeymanPackage.swift files --- ios/engine/KMEI/KeymanEngine.xcodeproj/project.pbxproj | 6 +++--- .../Classes/{Model => }/KeyboardKeymanPackage.swift | 0 .../KeymanEngine/Classes/{Model => }/KeymanPackage.swift | 0 .../Classes/{Model => }/LexicalModelKeymanPackage.swift | 0 4 files changed, 3 insertions(+), 3 deletions(-) rename ios/engine/KMEI/KeymanEngine/Classes/{Model => }/KeyboardKeymanPackage.swift (100%) rename ios/engine/KMEI/KeymanEngine/Classes/{Model => }/KeymanPackage.swift (100%) rename ios/engine/KMEI/KeymanEngine/Classes/{Model => }/LexicalModelKeymanPackage.swift (100%) diff --git a/ios/engine/KMEI/KeymanEngine.xcodeproj/project.pbxproj b/ios/engine/KMEI/KeymanEngine.xcodeproj/project.pbxproj index 5a5af1d7ac..40d20c1fec 100644 --- a/ios/engine/KMEI/KeymanEngine.xcodeproj/project.pbxproj +++ b/ios/engine/KMEI/KeymanEngine.xcodeproj/project.pbxproj @@ -760,9 +760,6 @@ C042ED5C1FC6A65A001D82F4 /* Version.swift */, C08E69901FDA6F6F0026056B /* FullKeyboardID.swift */, 9A079E42223B61AE00581263 /* FullLexicalModelID.swift */, - 1645D5942036C6FF0076C51B /* KeymanPackage.swift */, - 9A079E39223AFF3C00581263 /* KeyboardKeymanPackage.swift */, - 9A079DC9222E050E00581263 /* LexicalModelKeymanPackage.swift */, ); path = Model; sourceTree = ""; @@ -878,6 +875,9 @@ CE973D7F2484A07300F66045 /* KMP Packaging */ = { isa = PBXGroup; children = ( + 1645D5942036C6FF0076C51B /* KeymanPackage.swift */, + 9A079E39223AFF3C00581263 /* KeyboardKeymanPackage.swift */, + 9A079DC9222E050E00581263 /* LexicalModelKeymanPackage.swift */, CE867FF22485F38300B2EAED /* kmp.json */, ); name = "KMP Packaging"; diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Model/KeyboardKeymanPackage.swift b/ios/engine/KMEI/KeymanEngine/Classes/KeyboardKeymanPackage.swift similarity index 100% rename from ios/engine/KMEI/KeymanEngine/Classes/Model/KeyboardKeymanPackage.swift rename to ios/engine/KMEI/KeymanEngine/Classes/KeyboardKeymanPackage.swift diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Model/KeymanPackage.swift b/ios/engine/KMEI/KeymanEngine/Classes/KeymanPackage.swift similarity index 100% rename from ios/engine/KMEI/KeymanEngine/Classes/Model/KeymanPackage.swift rename to ios/engine/KMEI/KeymanEngine/Classes/KeymanPackage.swift diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Model/LexicalModelKeymanPackage.swift b/ios/engine/KMEI/KeymanEngine/Classes/LexicalModelKeymanPackage.swift similarity index 100% rename from ios/engine/KMEI/KeymanEngine/Classes/Model/LexicalModelKeymanPackage.swift rename to ios/engine/KMEI/KeymanEngine/Classes/LexicalModelKeymanPackage.swift From d903ce50268918b61d950debd7b6775717414f37 Mon Sep 17 00:00:00 2001 From: jahorton Date: Tue, 2 Jun 2020 11:15:00 +0700 Subject: [PATCH 02/11] refactor(ios/engine): refactors round 1 of kmp parsing --- .../Classes/KeyboardKeymanPackage.swift | 27 +++------ .../KeymanEngine/Classes/KeymanPackage.swift | 60 ++++++++----------- .../Classes/LexicalModelKeymanPackage.swift | 26 +++----- .../Classes/kmp.json/KMPMetadata.swift | 4 ++ 4 files changed, 45 insertions(+), 72 deletions(-) diff --git a/ios/engine/KMEI/KeymanEngine/Classes/KeyboardKeymanPackage.swift b/ios/engine/KMEI/KeymanEngine/Classes/KeyboardKeymanPackage.swift index f746a6b929..4f3036934e 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/KeyboardKeymanPackage.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/KeyboardKeymanPackage.swift @@ -8,28 +8,17 @@ import Foundation -public class KeyboardKeymanPackage : KeymanPackage -{ +public class KeyboardKeymanPackage : KeymanPackage { private var keyboards: [KMPKeyboard]! - - public override func parse(json: [String:AnyObject], version: String) { + + override init(metadata: KMPMetadata, folder: URL) { + super.init(metadata: metadata, folder: folder) self.keyboards = [] - if let packagedKeyboards = json["keyboards"] as? [[String:AnyObject]] { - for keyboardJson in packagedKeyboards { - do { - // A temporary hybrid state; we now transition to using a Decoder-based strategy. - let jsonData = try JSONSerialization.data(withJSONObject: keyboardJson, options: .prettyPrinted) - let decoder = JSONDecoder() - - let keyboard = try decoder.decode(KMPKeyboard.self, from: jsonData) - if(keyboard.isValid && FileManager.default.fileExists(atPath: self.sourceFolder.appendingPathComponent("\(keyboard.keyboardId).js").path)) { - keyboards.append(keyboard) - } - } catch { - // Simply... don't append a keyboard, like with the .isValid check above. - // Denotes original behavior of this method; isn't exactly optimal. - continue + if let packagedKeyboards = metadata.keyboards { + for keyboard in packagedKeyboards { + if(keyboard.isValid && FileManager.default.fileExists(atPath: self.sourceFolder.appendingPathComponent("\(keyboard.keyboardId).js").path)) { + keyboards.append(keyboard) } } } diff --git a/ios/engine/KMEI/KeymanEngine/Classes/KeymanPackage.swift b/ios/engine/KMEI/KeymanEngine/Classes/KeymanPackage.swift index 7235064674..61cf182118 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/KeymanPackage.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/KeymanPackage.swift @@ -10,18 +10,20 @@ import Foundation import Zip public enum KMPError : String, Error { + case noMetadata = "Could not find kmp.json for package." case invalidPackage = "Invalid Keyman Package." case fileSystem = "Unable to create directory structure in file system." case copyFiles = "Unable to copy keyboard files to file system." } -public class KeymanPackage -{ +public class KeymanPackage { static private let kmpFile = "kmp.json" - public var sourceFolder: URL + public let sourceFolder: URL + public let metadata: KMPMetadata - init(folder: URL) { + init(metadata: KMPMetadata, folder: URL) { sourceFolder = folder + self.metadata = metadata } // to be overridden by subclasses @@ -29,11 +31,6 @@ public class KeymanPackage return false } - // to be overridden by subclasses - public func parse(json: [String:AnyObject], version: String) { - return - } - // to be overridden by subclasses public func defaultInfoHtml() -> String { return "base class!" @@ -50,6 +47,10 @@ public class KeymanPackage return defaultInfoHtml() } + + public var version: String? { + return metadata.version + } static public func parse(_ folder: URL) -> KeymanPackage? { do { @@ -57,32 +58,23 @@ public class KeymanPackage path.appendPathComponent(kmpFile) if FileManager.default.fileExists(atPath: path.path) { let data = try Data(contentsOf: path, options: .mappedIfSafe) - let jsonResult = try JSONSerialization.jsonObject(with: data, options: .mutableLeaves) - if let jsonResult = jsonResult as? [String:AnyObject] { - // The 'version' info has moved, and is no longer available in the original - // location for lexical models. Still present for keyboards, though! - var version: String = "" - if let info = jsonResult["info"] as? [String:AnyObject] { - if let versionBlock = info["version"] as? [String:AnyObject] { - version = versionBlock["description"] as? String ?? "" - } - } + let decoder = JSONDecoder() - if let _ = jsonResult["keyboards"] as? [[String:AnyObject]] { - if let _ = jsonResult["lexicalModels"] as? [[String:AnyObject]] { - //TODO: rrb show error to user, for now, just log - log.error("error parsing keyman package: packages MUST NOT have both keyboards and lexical models") - return nil - } - let kmp = KeyboardKeymanPackage.init(folder: folder) - kmp.parse(json: jsonResult, version: version) - return kmp - } - else if let _ = jsonResult["lexicalModels"] as? [[String:AnyObject]] { - let kmm = LexicalModelKeymanPackage.init(folder: folder) - kmm.parse(json: jsonResult, version: version) - return kmm - } + var metadata: KMPMetadata + + do { + metadata = try decoder.decode(KMPMetadata.self, from: data) + } catch { + throw KMPError.noMetadata + } + + switch metadata.packageType { + case .Keyboard: + return KeyboardKeymanPackage(metadata: metadata, folder: folder) + case .LexicalModel: + return LexicalModelKeymanPackage(metadata: metadata, folder: folder) + default: + throw KMPError.invalidPackage } } } catch { diff --git a/ios/engine/KMEI/KeymanEngine/Classes/LexicalModelKeymanPackage.swift b/ios/engine/KMEI/KeymanEngine/Classes/LexicalModelKeymanPackage.swift index 538f4e4672..4b841879d7 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/LexicalModelKeymanPackage.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/LexicalModelKeymanPackage.swift @@ -19,30 +19,18 @@ public class LexicalModelKeymanPackage : KeymanPackage { } return str } - - // may be called parse() - // returns a dictionary mapping language ID to lexical model ID - override public func parse(json: [String:AnyObject], version: String) { + + override init(metadata: KMPMetadata, folder: URL) { + super.init(metadata: metadata, folder: folder) self.models = [] - if let packagedModels = json["lexicalModels"] as? [[String:AnyObject]] { - for modelJson in packagedModels { - // A temporary hybrid state; we now transition to using a Decoder-based strategy. - do { - let jsonData = try JSONSerialization.data(withJSONObject: modelJson, options: .prettyPrinted) - let decoder = JSONDecoder() - - let model = try decoder.decode(KMPLexicalModel.self, from: jsonData) - if(model.isValid && FileManager.default.fileExists(atPath: self.sourceFolder.appendingPathComponent("\(model.lexicalModelId).model.js").path)) { - models.append(model) - } - } catch { - // Append no models. Not the greatest strategy, but it's the one that had always - // been taken here. + if let packagedModels = metadata.lexicalModels { + for model in packagedModels { + if(model.isValid && FileManager.default.fileExists(atPath: self.sourceFolder.appendingPathComponent("\(model.lexicalModelId).model.js").path)) { + models.append(model) } } } } - } diff --git a/ios/engine/KMEI/KeymanEngine/Classes/kmp.json/KMPMetadata.swift b/ios/engine/KMEI/KeymanEngine/Classes/kmp.json/KMPMetadata.swift index d8d997d1d5..4d09e9fe79 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/kmp.json/KMPMetadata.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/kmp.json/KMPMetadata.swift @@ -57,4 +57,8 @@ public class KMPMetadata: Codable { return .Unsupported } } + + var version: String? { + return self.info?.version?.description + } } From 63d088f852d3d28d910cf5a887f82e9694a9f458 Mon Sep 17 00:00:00 2001 From: jahorton Date: Wed, 3 Jun 2020 08:21:17 +0700 Subject: [PATCH 03/11] change(ios/engine): retools KMPMetadata test assertions for reuse --- .../KMEI/KeymanEngineTests/KMPJSONTests.swift | 18 ++++++++++++------ .../TestUtils/LexicalModels.swift | 6 +++++- .../kmp-json-nrc.en.mtnt.json | 4 ++-- 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/ios/engine/KMEI/KeymanEngineTests/KMPJSONTests.swift b/ios/engine/KMEI/KeymanEngineTests/KMPJSONTests.swift index 9659922e9d..ec464f7ea4 100644 --- a/ios/engine/KMEI/KeymanEngineTests/KMPJSONTests.swift +++ b/ios/engine/KMEI/KeymanEngineTests/KMPJSONTests.swift @@ -68,9 +68,7 @@ class KMPJSONTests: XCTestCase { lexical_model_nrc_en_mtnt_assertions(nrc_en_mtnt) } - func testKeyboardPackageInfoDecoding() throws { - let khmer_angkor: KMPMetadata = try loadObjectFromJSON(at: TestUtils.PackageJSON.kmp_json_khmer_angkor) - + func kmp_info_khmer_angkor_assertions(_ khmer_angkor: KMPMetadata) { XCTAssertTrue(khmer_angkor.isValid) XCTAssertEqual(khmer_angkor.packageType, KMPMetadata.PackageType.Keyboard) @@ -95,9 +93,12 @@ class KMPJSONTests: XCTestCase { XCTAssertNil(khmer_angkor.options.readmeFile) } - func testLexicalModelPackageInfoDecoding() throws { - let nrc_en_mtnt: KMPMetadata = try loadObjectFromJSON(at: TestUtils.PackageJSON.kmp_json_nrc_en_mtnt) + func testKeyboardPackageInfoDecoding() throws { + let khmer_angkor: KMPMetadata = try loadObjectFromJSON(at: TestUtils.PackageJSON.kmp_json_khmer_angkor) + kmp_info_khmer_angkor_assertions(khmer_angkor) + } + func kmp_info_nrc_en_mtnt_assertions(_ nrc_en_mtnt: KMPMetadata) { XCTAssertTrue(nrc_en_mtnt.isValid) XCTAssertEqual(nrc_en_mtnt.packageType, KMPMetadata.PackageType.LexicalModel) @@ -111,7 +112,7 @@ class KMPJSONTests: XCTestCase { XCTAssertEqual(nrc_en_mtnt.files!.count, 1) XCTAssertNotNil(nrc_en_mtnt.info) - XCTAssertEqual(nrc_en_mtnt.info!.version!.description, "0.1.3") + XCTAssertEqual(nrc_en_mtnt.info!.version!.description, "0.1.4") XCTAssertNil(nrc_en_mtnt.info!.version!.url) XCTAssertEqual(nrc_en_mtnt.info!.author!.description, "Eddie Antonio Santos") XCTAssertEqual(nrc_en_mtnt.info!.author!.url, "mailto:easantos@ualberta.ca") @@ -121,4 +122,9 @@ class KMPJSONTests: XCTestCase { XCTAssertNil(nrc_en_mtnt.options.graphicFile) XCTAssertNil(nrc_en_mtnt.options.readmeFile) } + + func testLexicalModelPackageInfoDecoding() throws { + let nrc_en_mtnt: KMPMetadata = try loadObjectFromJSON(at: TestUtils.PackageJSON.kmp_json_nrc_en_mtnt) + kmp_info_nrc_en_mtnt_assertions(nrc_en_mtnt) + } } diff --git a/ios/engine/KMEI/KeymanEngineTests/TestUtils/LexicalModels.swift b/ios/engine/KMEI/KeymanEngineTests/TestUtils/LexicalModels.swift index b79090a3dc..6781a288b6 100644 --- a/ios/engine/KMEI/KeymanEngineTests/TestUtils/LexicalModels.swift +++ b/ios/engine/KMEI/KeymanEngineTests/TestUtils/LexicalModels.swift @@ -12,6 +12,10 @@ import Foundation extension TestUtils { enum LexicalModels { static let mtntKMP = TestUtils.lexicalModelsBundle.url(forResource: "nrc.en.mtnt.model", withExtension: "kmp")! - static let mtnt = Defaults.lexicalModel + static let mtnt = InstallableLexicalModel(id: "nrc.en.mtnt", + name: "English dictionary (MTNT)", + languageID: "en", + version: "0.1.4", + isCustom: false) } } diff --git a/ios/engine/KMEI/KeymanEngineTests/resources/PackageJSON.bundle/kmp-json-nrc.en.mtnt.json b/ios/engine/KMEI/KeymanEngineTests/resources/PackageJSON.bundle/kmp-json-nrc.en.mtnt.json index 0793eb5fe5..e39d5e6ca4 100644 --- a/ios/engine/KMEI/KeymanEngineTests/resources/PackageJSON.bundle/kmp-json-nrc.en.mtnt.json +++ b/ios/engine/KMEI/KeymanEngineTests/resources/PackageJSON.bundle/kmp-json-nrc.en.mtnt.json @@ -4,7 +4,7 @@ "fileVersion": "12.0" }, "options": { - "followKeyboardVersion": true + "followKeyboardVersion": false }, "info": { "author": { @@ -18,7 +18,7 @@ "description": "English language model mined from MTNT" }, "version": { - "description": "0.1.3" + "description": "0.1.4" } }, "files": [ From 0b2b90c90239ae7430e9b612c3780f51a7d054c7 Mon Sep 17 00:00:00 2001 From: jahorton Date: Wed, 3 Jun 2020 08:32:51 +0700 Subject: [PATCH 04/11] refactor(ios/engine): KMPMetadata assertions on package extraction tests --- .../KeymanEngine.xcodeproj/project.pbxproj | 4 + .../FileManagementTests.swift | 50 ------------- .../KeymanPackageTests.swift | 74 +++++++++++++++++++ 3 files changed, 78 insertions(+), 50 deletions(-) create mode 100644 ios/engine/KMEI/KeymanEngineTests/KeymanPackageTests.swift diff --git a/ios/engine/KMEI/KeymanEngine.xcodeproj/project.pbxproj b/ios/engine/KMEI/KeymanEngine.xcodeproj/project.pbxproj index 40d20c1fec..166151bdd4 100644 --- a/ios/engine/KMEI/KeymanEngine.xcodeproj/project.pbxproj +++ b/ios/engine/KMEI/KeymanEngine.xcodeproj/project.pbxproj @@ -179,6 +179,7 @@ CE867FEF2485E97A00B2EAED /* KMPMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE867FEE2485E97A00B2EAED /* KMPMetadata.swift */; }; CE867FF12485EE1500B2EAED /* KMPInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE867FF02485EE1500B2EAED /* KMPInfo.swift */; }; CE88042F247F4B97009BCA1A /* ExpectationDownloaderDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE88042E247F4B97009BCA1A /* ExpectationDownloaderDelegate.swift */; }; + CE8B0BBD248734240045EB2E /* KeymanPackageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE8B0BBC248734240045EB2E /* KeymanPackageTests.swift */; }; CE8EDEB123F53D1A009E1FF6 /* FileManagementTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A079DD1223194B100581263 /* FileManagementTests.swift */; }; CE8EDEB323F53F96009E1FF6 /* VersionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE8EDEB223F53F96009E1FF6 /* VersionTests.swift */; }; CE973D812484A56500F66045 /* PackageJSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE973D802484A56500F66045 /* PackageJSON.swift */; }; @@ -460,6 +461,7 @@ CE867FEE2485E97A00B2EAED /* KMPMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KMPMetadata.swift; sourceTree = ""; }; CE867FF02485EE1500B2EAED /* KMPInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KMPInfo.swift; sourceTree = ""; }; CE88042E247F4B97009BCA1A /* ExpectationDownloaderDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpectationDownloaderDelegate.swift; sourceTree = ""; }; + CE8B0BBC248734240045EB2E /* KeymanPackageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeymanPackageTests.swift; sourceTree = ""; }; CE8EDEB223F53F96009E1FF6 /* VersionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionTests.swift; sourceTree = ""; }; CE973D802484A56500F66045 /* PackageJSON.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PackageJSON.swift; sourceTree = ""; }; CE973D822484A5DB00F66045 /* PackageJSON.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = PackageJSON.bundle; sourceTree = ""; }; @@ -708,6 +710,7 @@ CE9CD88123FCC4C7002BF2F8 /* TestUtils */, CE37C38923FCC9AF007031C6 /* resources */, 9A079DD1223194B100581263 /* FileManagementTests.swift */, + CE8B0BBC248734240045EB2E /* KeymanPackageTests.swift */, CE07E3D1247E3FB100DFA9D2 /* HTTPDownloaderTests.swift */, 9A079DD3223194B100581263 /* Info.plist */, CE8EDEB223F53F96009E1FF6 /* VersionTests.swift */, @@ -1390,6 +1393,7 @@ CE37C39123FCD617007031C6 /* LexicalModels.swift in Sources */, CE37C38D23FCD41E007031C6 /* Keyboards.swift in Sources */, CE9CD88323FCC4E3002BF2F8 /* UserDefaults.swift in Sources */, + CE8B0BBD248734240045EB2E /* KeymanPackageTests.swift in Sources */, CE973D852484A71700F66045 /* KMPJSONTests.swift in Sources */, CEF888D523FE786C00667693 /* KeyboardScaleTests.swift in Sources */, CE07E3D6247E41DB00DFA9D2 /* URLSessionMock.swift in Sources */, diff --git a/ios/engine/KMEI/KeymanEngineTests/FileManagementTests.swift b/ios/engine/KMEI/KeymanEngineTests/FileManagementTests.swift index b760cfe4f0..b6dbb780ca 100644 --- a/ios/engine/KMEI/KeymanEngineTests/FileManagementTests.swift +++ b/ios/engine/KMEI/KeymanEngineTests/FileManagementTests.swift @@ -21,56 +21,6 @@ class FileManagementTests: XCTestCase { TestUtils.standardTearDown() } - func testKeyboardPackageExtraction() throws { - let cacheDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0] - let khmerPackageZip = cacheDirectory.appendingPathComponent("khmer_angkor.zip") - try FileManager.default.copyItem(at: TestUtils.Keyboards.khmerAngkorKMP, to: khmerPackageZip) - - let destinationFolderURL = cacheDirectory.appendingPathComponent("khmer_angkor") - - // Requires that the source file is already .zip, not .kmp. It's a ZipUtils limitation. - do { - try KeymanPackage.extract(fileUrl: khmerPackageZip, destination: destinationFolderURL, complete: { kmp in - if let kmp = kmp { - XCTAssertTrue(kmp.isKeyboard(), "Keyboard KMP test extraction did not yield a keyboard package!") - - // extracted ok, test kmp - XCTAssert(kmp.sourceFolder == destinationFolderURL, - "The KMP's reported 'source folder' should match the specified destination folder") - } else { - XCTAssert(false, "KeymanPackage.extract failed") - } - }) - } catch { - XCTFail("KeymanPackage.extract failed with error \(error)") - } - } - - func testLexicalModelPackageExtraction() throws { - let cacheDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0] - let mtntZip = cacheDirectory.appendingPathComponent("mtnt.zip") - try FileManager.default.copyItem(at: TestUtils.LexicalModels.mtntKMP, to: mtntZip) - - let destinationFolderURL = cacheDirectory.appendingPathComponent("mtnt.model") - - // Requires that the source file is already .zip, not .kmp. It's a ZipUtils limitation. - do { - try KeymanPackage.extract(fileUrl: mtntZip, destination: destinationFolderURL, complete: { kmp in - if let kmp = kmp { - XCTAssertTrue(!kmp.isKeyboard(), "Lexical model KMP test extraction yielded a keyboard package!") - - // extracted ok, test kmp - XCTAssert(kmp.sourceFolder == destinationFolderURL, - "The KMP's reported 'source folder' should match the specified destination folder") - } else { - XCTAssert(false, "KeymanPackage.extract failed") - } - }) - } catch { - XCTFail("KeymanPackage.extract failed with error \(error)") - } - } - func testKeyboardInstallation() { ResourceFileManager.shared.prepareKMPInstall(from: TestUtils.Keyboards.khmerAngkorKMP) { kmp, error in XCTAssertNotNil(kmp, "Failed to prepare KMP for installation") diff --git a/ios/engine/KMEI/KeymanEngineTests/KeymanPackageTests.swift b/ios/engine/KMEI/KeymanEngineTests/KeymanPackageTests.swift new file mode 100644 index 0000000000..ae068796cb --- /dev/null +++ b/ios/engine/KMEI/KeymanEngineTests/KeymanPackageTests.swift @@ -0,0 +1,74 @@ +// +// KeymanPackageTests.swift +// KeymanEngineTests +// +// Created by Joshua Horton on 6/3/20. +// Copyright © 2020 SIL International. All rights reserved. +// + +import XCTest +@testable import KeymanEngine + +class KeymanPackageTests: XCTestCase { + func testKeyboardPackageExtraction() throws { + let cacheDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0] + let khmerPackageZip = cacheDirectory.appendingPathComponent("khmer_angkor.zip") + try FileManager.default.copyItem(at: TestUtils.Keyboards.khmerAngkorKMP, to: khmerPackageZip) + + let destinationFolderURL = cacheDirectory.appendingPathComponent("khmer_angkor") + + // Requires that the source file is already .zip, not .kmp. It's a ZipUtils limitation. + do { + try KeymanPackage.extract(fileUrl: khmerPackageZip, destination: destinationFolderURL, complete: { kmp in + if let kmp = kmp { + // Run assertions on the package's kmp.info. + // Assumes the KMP used for testing here has the same kmp.info used for those tests. + let kmp_json_testcase = KMPJSONTests() + kmp_json_testcase.kmp_info_khmer_angkor_assertions(kmp.metadata) + + XCTAssertNotNil(kmp as? KeyboardKeymanPackage, "Keyboard KMP test extraction did not yield a keyboard package!") + XCTAssertTrue(kmp.isKeyboard(), "Keyboard KMP test extraction did not yield a keyboard package!") + + // extracted ok, test kmp + XCTAssert(kmp.sourceFolder == destinationFolderURL, + "The KMP's reported 'source folder' should match the specified destination folder") + } else { + XCTAssert(false, "KeymanPackage.extract failed") + } + }) + } catch { + XCTFail("KeymanPackage.extract failed with error \(error)") + } + } + + func testLexicalModelPackageExtraction() throws { + let cacheDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0] + let mtntZip = cacheDirectory.appendingPathComponent("mtnt.zip") + try FileManager.default.copyItem(at: TestUtils.LexicalModels.mtntKMP, to: mtntZip) + + let destinationFolderURL = cacheDirectory.appendingPathComponent("mtnt.model") + + // Requires that the source file is already .zip, not .kmp. It's a ZipUtils limitation. + do { + try KeymanPackage.extract(fileUrl: mtntZip, destination: destinationFolderURL, complete: { kmp in + if let kmp = kmp { + // Run assertions on the package's kmp.info. + // Assumes the KMP used for testing here has the same kmp.info used for those tests. + let kmp_json_testcase = KMPJSONTests() + kmp_json_testcase.kmp_info_nrc_en_mtnt_assertions(kmp.metadata) + + XCTAssertNotNil(kmp as? LexicalModelKeymanPackage, "Lexical model KMP test extraction yielded a keyboard package!") + XCTAssertTrue(!kmp.isKeyboard(), "Lexical model KMP test extraction yielded a keyboard package!") + + // extracted ok, test kmp + XCTAssert(kmp.sourceFolder == destinationFolderURL, + "The KMP's reported 'source folder' should match the specified destination folder") + } else { + XCTAssert(false, "KeymanPackage.extract failed") + } + }) + } catch { + XCTFail("KeymanPackage.extract failed with error \(error)") + } + } +} From c44958e395cf6252727778c437695167cf10c55a Mon Sep 17 00:00:00 2001 From: jahorton Date: Wed, 3 Jun 2020 08:43:40 +0700 Subject: [PATCH 05/11] change(ios/engine): test tweak, uses KMPMetadata for .isKeyboard helper --- .../Classes/KeyboardKeymanPackage.swift | 5 ----- .../KeymanEngine/Classes/KeymanPackage.swift | 2 +- .../Classes/LexicalModelKeymanPackage.swift | 18 +++++++++--------- .../FileManagementTests.swift | 4 ++-- 4 files changed, 12 insertions(+), 17 deletions(-) diff --git a/ios/engine/KMEI/KeymanEngine/Classes/KeyboardKeymanPackage.swift b/ios/engine/KMEI/KeymanEngine/Classes/KeyboardKeymanPackage.swift index 3b8446208c..ffb2ee74a9 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/KeyboardKeymanPackage.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/KeyboardKeymanPackage.swift @@ -33,10 +33,5 @@ public class KeyboardKeymanPackage : KeymanPackage { } return str } - - public override func isKeyboard() -> Bool { - return true - } - } diff --git a/ios/engine/KMEI/KeymanEngine/Classes/KeymanPackage.swift b/ios/engine/KMEI/KeymanEngine/Classes/KeymanPackage.swift index 61cf182118..212a16822e 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/KeymanPackage.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/KeymanPackage.swift @@ -28,7 +28,7 @@ public class KeymanPackage { // to be overridden by subclasses public func isKeyboard() -> Bool { - return false + return metadata.packageType == .Keyboard } // to be overridden by subclasses diff --git a/ios/engine/KMEI/KeymanEngine/Classes/LexicalModelKeymanPackage.swift b/ios/engine/KMEI/KeymanEngine/Classes/LexicalModelKeymanPackage.swift index 29995c51ad..85755b8d8f 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/LexicalModelKeymanPackage.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/LexicalModelKeymanPackage.swift @@ -10,15 +10,6 @@ import Foundation public class LexicalModelKeymanPackage : KeymanPackage { public var models : [KMPLexicalModel]! - - public override func defaultInfoHtml() -> String { - let count = models.count - var str = "Found "+(count > 1 ? "\(count) dictionaries" : "dictionary")+" in package:
" - for model in models { - str += model.lexicalModelId + "
" - } - return str - } override init(metadata: KMPMetadata, folder: URL) { super.init(metadata: metadata, folder: folder) @@ -34,5 +25,14 @@ public class LexicalModelKeymanPackage : KeymanPackage { } } } + + public override func defaultInfoHtml() -> String { + let count = models.count + var str = "Found "+(count > 1 ? "\(count) dictionaries" : "dictionary")+" in package:
" + for model in models { + str += model.lexicalModelId + "
" + } + return str + } } diff --git a/ios/engine/KMEI/KeymanEngineTests/FileManagementTests.swift b/ios/engine/KMEI/KeymanEngineTests/FileManagementTests.swift index b6dbb780ca..59efcb087d 100644 --- a/ios/engine/KMEI/KeymanEngineTests/FileManagementTests.swift +++ b/ios/engine/KMEI/KeymanEngineTests/FileManagementTests.swift @@ -25,7 +25,7 @@ class FileManagementTests: XCTestCase { 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") - XCTAssertTrue(kmp!.isKeyboard(), "KMP resource type improperly recognized - expected a keyboard package!") + XCTAssertNotNil(kmp as? KeyboardKeymanPackage, "KMP resource type improperly recognized - expected a keyboard package!") ResourceFileManager.shared.finalizePackageInstall(kmp!, isCustom: true) { innerError in XCTAssertNil(innerError, "Error occurred while finalizing KMP installation") @@ -47,7 +47,7 @@ class FileManagementTests: XCTestCase { 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") - XCTAssertFalse(kmp!.isKeyboard(), "KMP resource type improperly recognized - expected a lexical model package!") + XCTAssertNotNil(kmp as? LexicalModelKeymanPackage, "KMP resource type improperly recognized - expected a lexical model package!") ResourceFileManager.shared.finalizePackageInstall(kmp!, isCustom: true) { innerError in XCTAssertNil(innerError, "Error occurred while finalizing KMP installation") From c25afce07fc7821b0cfec272835b21e31b1ed7dd Mon Sep 17 00:00:00 2001 From: jahorton Date: Wed, 3 Jun 2020 09:11:50 +0700 Subject: [PATCH 06/11] refactor(ios/engine): generalized cleanup, lex model version defaults --- .../Classes/LexicalModelKeymanPackage.swift | 2 ++ .../Resource Management/ResourceDownloadQueue.swift | 2 -- .../KeymanEngine/Classes/kmp.json/KMPKeyboard.swift | 6 +++--- .../Classes/kmp.json/KMPLexicalModel.swift | 12 +++++++++--- ios/engine/KMEI/KeymanEngineTests/KMPJSONTests.swift | 8 ++++---- .../KMEI/KeymanEngineTests/KeymanPackageTests.swift | 5 ++++- 6 files changed, 22 insertions(+), 13 deletions(-) diff --git a/ios/engine/KMEI/KeymanEngine/Classes/LexicalModelKeymanPackage.swift b/ios/engine/KMEI/KeymanEngine/Classes/LexicalModelKeymanPackage.swift index 85755b8d8f..85e7df6e98 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/LexicalModelKeymanPackage.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/LexicalModelKeymanPackage.swift @@ -17,6 +17,8 @@ public class LexicalModelKeymanPackage : KeymanPackage { if let packagedModels = metadata.lexicalModels { for model in packagedModels { + // If completely missing, we set the version to 1.0. Legacy decision from 2005. + model.setNilVersion(to: metadata.info?.version?.description ?? "1.0") if(model.isValid && FileManager.default.fileExists(atPath: self.sourceFolder.appendingPathComponent("\(model.lexicalModelId).model.js").path)) { models.append(model) } else { diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/ResourceDownloadQueue.swift b/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/ResourceDownloadQueue.swift index 72113df82b..091e639c86 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/ResourceDownloadQueue.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/ResourceDownloadQueue.swift @@ -414,8 +414,6 @@ class ResourceDownloadQueue: HTTPDownloadDelegate { // The request has succeeded. if downloader!.requestsCount == 0 { // Download queue finished. let keyboards = batch.resources as! [InstallableKeyboard] - - FontManager.shared.registerCustomFonts() log.info("Downloaded keyboard: \(keyboards[0].id).") if(!isUpdate) { diff --git a/ios/engine/KMEI/KeymanEngine/Classes/kmp.json/KMPKeyboard.swift b/ios/engine/KMEI/KeymanEngine/Classes/kmp.json/KMPKeyboard.swift index 7826c1e0e2..53d86aa847 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/kmp.json/KMPKeyboard.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/kmp.json/KMPKeyboard.swift @@ -18,7 +18,6 @@ public class KMPKeyboard: Codable { // Technically, the schema says that this doesn't have to be specified. // However, it's a requirement for KeymanEngine for iOS. public var languages: [KMPLanguage] - public var installableKeyboards: [InstallableKeyboard]! = [] enum CodingKeys: String, CodingKey { case name @@ -40,8 +39,9 @@ public class KMPKeyboard: Codable { font = try values.decodeIfPresent(String.self, forKey: .font) isRTL = try values.decodeIfPresent(Bool.self, forKey: .isRTL) ?? false languages = try values.decode([KMPLanguage].self, forKey: .languages) + } - // Now to build the InstallableKeyboard list. + public var installableKeyboards: [InstallableKeyboard] { var installableKeyboards : [InstallableKeyboard] = [] for language in self.languages { @@ -57,7 +57,7 @@ public class KMPKeyboard: Codable { installableKeyboards.append( keyboard ) } - self.installableKeyboards = installableKeyboards + return installableKeyboards } public var displayFont: Font? { diff --git a/ios/engine/KMEI/KeymanEngine/Classes/kmp.json/KMPLexicalModel.swift b/ios/engine/KMEI/KeymanEngine/Classes/kmp.json/KMPLexicalModel.swift index 41931936d3..bea8fbaeaf 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/kmp.json/KMPLexicalModel.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/kmp.json/KMPLexicalModel.swift @@ -14,7 +14,6 @@ public class KMPLexicalModel: Codable { public var version: String? public var isRTL: Bool = false public var languages: [KMPLanguage] - public var installableLexicalModels: [InstallableLexicalModel]! = [] enum CodingKeys: String, CodingKey { case name @@ -35,8 +34,15 @@ public class KMPLexicalModel: Codable { version = try values.decodeIfPresent(String.self, forKey: .version) isRTL = try values.decodeIfPresent(Bool.self, forKey: .isRTL) ?? false languages = try values.decode([KMPLanguage].self, forKey: .languages) + } - // Now to build the InstallableKeyboard list. + /** This function will set the value for version only when a lexical model doesn't have a value for it specified directly in its definition. + */ + public func setNilVersion(to version: String) { + self.version = self.version ?? version + } + + public var installableLexicalModels: [InstallableLexicalModel] { var installableLexicalModels : [InstallableLexicalModel] = [] for language in self.languages { @@ -48,7 +54,7 @@ public class KMPLexicalModel: Codable { installableLexicalModels.append( model ) } - self.installableLexicalModels = installableLexicalModels + return installableLexicalModels } public var isValid: Bool { diff --git a/ios/engine/KMEI/KeymanEngineTests/KMPJSONTests.swift b/ios/engine/KMEI/KeymanEngineTests/KMPJSONTests.swift index ec464f7ea4..a6964ad8d0 100644 --- a/ios/engine/KMEI/KeymanEngineTests/KMPJSONTests.swift +++ b/ios/engine/KMEI/KeymanEngineTests/KMPJSONTests.swift @@ -49,12 +49,12 @@ class KMPJSONTests: XCTestCase { } // A helper method, since these are useful in two separate tests. - func lexical_model_nrc_en_mtnt_assertions(_ nrc_en_mtnt: KMPLexicalModel) { + func lexical_model_nrc_en_mtnt_assertions(_ nrc_en_mtnt: KMPLexicalModel, version: String? = nil) { XCTAssertEqual(nrc_en_mtnt.name, "English dictionary (MTNT)") XCTAssertEqual(nrc_en_mtnt.lexicalModelId, "nrc.en.mtnt") // Our example case does not define either of these two values. // One (isRTL), we assume a default value for. - XCTAssertEqual(nrc_en_mtnt.version, nil) + XCTAssertEqual(nrc_en_mtnt.version, version) XCTAssertEqual(nrc_en_mtnt.isRTL, false) XCTAssertEqual(nrc_en_mtnt.languages.count, 3) @@ -98,13 +98,13 @@ class KMPJSONTests: XCTestCase { kmp_info_khmer_angkor_assertions(khmer_angkor) } - func kmp_info_nrc_en_mtnt_assertions(_ nrc_en_mtnt: KMPMetadata) { + func kmp_info_nrc_en_mtnt_assertions(_ nrc_en_mtnt: KMPMetadata, version: String? = nil) { XCTAssertTrue(nrc_en_mtnt.isValid) XCTAssertEqual(nrc_en_mtnt.packageType, KMPMetadata.PackageType.LexicalModel) XCTAssertNotNil(nrc_en_mtnt.lexicalModels) XCTAssertEqual(nrc_en_mtnt.lexicalModels!.count, 1) - lexical_model_nrc_en_mtnt_assertions(nrc_en_mtnt.lexicalModels![0]) + lexical_model_nrc_en_mtnt_assertions(nrc_en_mtnt.lexicalModels![0], version: version) XCTAssertNil(nrc_en_mtnt.keyboards) diff --git a/ios/engine/KMEI/KeymanEngineTests/KeymanPackageTests.swift b/ios/engine/KMEI/KeymanEngineTests/KeymanPackageTests.swift index ae068796cb..8a5046bcc4 100644 --- a/ios/engine/KMEI/KeymanEngineTests/KeymanPackageTests.swift +++ b/ios/engine/KMEI/KeymanEngineTests/KeymanPackageTests.swift @@ -55,7 +55,10 @@ class KeymanPackageTests: XCTestCase { // Run assertions on the package's kmp.info. // Assumes the KMP used for testing here has the same kmp.info used for those tests. let kmp_json_testcase = KMPJSONTests() - kmp_json_testcase.kmp_info_nrc_en_mtnt_assertions(kmp.metadata) + + // As this test takes place after construction of the LexicalModelPackage, + // the version will be set accordingly, unlike in the other JSON-related tests. + kmp_json_testcase.kmp_info_nrc_en_mtnt_assertions(kmp.metadata, version: "0.1.4") XCTAssertNotNil(kmp as? LexicalModelKeymanPackage, "Lexical model KMP test extraction yielded a keyboard package!") XCTAssertTrue(!kmp.isKeyboard(), "Lexical model KMP test extraction yielded a keyboard package!") From 96ecebed5f3c97d80873483aca09291dc65e5fec Mon Sep 17 00:00:00 2001 From: jahorton Date: Wed, 3 Jun 2020 09:42:28 +0700 Subject: [PATCH 07/11] refactor(ios/engine): parseKbdKMP now relies on KeymanPackage parsing --- .../Classes/KeyboardKeymanPackage.swift | 2 +- .../KeymanEngine/Classes/KeymanPackage.swift | 1 + .../KMEI/KeymanEngine/Classes/Manager.swift | 136 ++++++------------ 3 files changed, 47 insertions(+), 92 deletions(-) diff --git a/ios/engine/KMEI/KeymanEngine/Classes/KeyboardKeymanPackage.swift b/ios/engine/KMEI/KeymanEngine/Classes/KeyboardKeymanPackage.swift index ffb2ee74a9..61ef2101f1 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/KeyboardKeymanPackage.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/KeyboardKeymanPackage.swift @@ -9,7 +9,7 @@ import Foundation public class KeyboardKeymanPackage : KeymanPackage { - private var keyboards: [KMPKeyboard]! + public var keyboards: [KMPKeyboard]! override init(metadata: KMPMetadata, folder: URL) { super.init(metadata: metadata, folder: folder) diff --git a/ios/engine/KMEI/KeymanEngine/Classes/KeymanPackage.swift b/ios/engine/KMEI/KeymanEngine/Classes/KeymanPackage.swift index 212a16822e..2f81cecc7c 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/KeymanPackage.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/KeymanPackage.swift @@ -14,6 +14,7 @@ public enum KMPError : String, Error { case invalidPackage = "Invalid Keyman Package." 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" } public class KeymanPackage { diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Manager.swift b/ios/engine/KMEI/KeymanEngine/Classes/Manager.swift index 640680f039..9a784887a0 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/Manager.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/Manager.swift @@ -613,102 +613,56 @@ public class Manager: NSObject, UIGestureRecognizerDelegate { // MARK: - Adhoc keyboards public func parseKbdKMP(_ folder: URL, isCustom: Bool) throws -> Void { - do { - var path = folder - path.appendPathComponent("kmp.json") - let data = try Data(contentsOf: path, options: .mappedIfSafe) - let jsonResult = try JSONSerialization.jsonObject(with: data, options: .mutableLeaves) - if let jsonResult = jsonResult as? [String:AnyObject] { - if let keyboards = jsonResult["keyboards"] as? [[String:AnyObject]] { - for k in keyboards { - let name = k["name"] as! String - let keyboardID = k["id"] as! String - let version = k["version"] as! String - //true if the keyboard targets a right-to-left script. false if absent. - let isrtl: Bool = k["rtl"] as? Bool ?? false + guard let kmp = KeymanPackage.parse(folder) as? KeyboardKeymanPackage else { + throw KMPError.wrongPackageType + } - var oskFont: Font? - let osk = k["oskFont"] as? String - if let _ = osk { - oskFont = Font(filename: osk!) - } - var displayFont: Font? - let font = k["displayFont"] as? String - if let _ = font { - displayFont = Font(filename: font!) - } - - //TODO: handle errors if languages do not exist - var languageName = "" - var languageId = "" - - var installableKeyboards : [InstallableKeyboard] = [] - if let langs = k["languages"] as? [[String:String]] { - for l in langs { - languageName = l["name"]! - languageId = l["id"]! - - installableKeyboards.append( InstallableKeyboard( - id: keyboardID, - name: name, - languageID: languageId, - languageName: languageName, - version: version, - isRTL: isrtl, - font: displayFont, - oskFont: oskFont, - isCustom: isCustom)) - } - } - - do { - try FileManager.default.createDirectory(at: Storage.active.keyboardDir(forID: keyboardID), - withIntermediateDirectories: true) - } catch { - log.error("Could not create dir for download: \(error)") - throw KMPError.fileSystem - } - - var haveInstalledOne = false - for keyboard in installableKeyboards { - let storedPath = Storage.active.keyboardURL(for: keyboard) - - var installableFiles: [[Any]] = [["\(keyboardID).js", storedPath]] - if let osk = osk { - let oskPath = Storage.active.fontURL(forKeyboardID: keyboardID, filename: osk) - installableFiles.append([osk, oskPath]) - } - - if let font = font { - let displayPath = Storage.active.fontURL(forKeyboardID: keyboardID, filename: font) - installableFiles.append([font, displayPath]) - } - do { - for item in installableFiles { - var filePath = folder - if(FileManager.default.fileExists(atPath: (item[1] as! URL).path)) { - try FileManager.default.removeItem(at: item[1] as! URL) - } - filePath.appendPathComponent(item[0] as! String) - try FileManager.default.copyItem(at: filePath, - to: item[1] as! URL) - - } - } catch { - log.error("Error saving the download: \(error)") - throw KMPError.copyFiles - } - if !haveInstalledOne { - Manager.shared.addKeyboard(keyboard) - haveInstalledOne = true - } + for k in kmp.keyboards { + let installableKeyboards : [InstallableKeyboard] = k.installableKeyboards + let keyboardID = k.keyboardId + + do { + try FileManager.default.createDirectory(at: Storage.active.keyboardDir(forID: keyboardID), + withIntermediateDirectories: true) + } catch { + log.error("Could not create dir for download: \(error)") + throw KMPError.fileSystem + } + + var haveInstalledOne = false + for keyboard in installableKeyboards { + let storedPath = Storage.active.keyboardURL(for: keyboard) + + var installableFiles: [[Any]] = [["\(keyboardID).js", storedPath]] + if let osk = k.osk { + let oskPath = Storage.active.fontURL(forKeyboardID: keyboardID, filename: osk) + installableFiles.append([osk, oskPath]) + } + + if let font = k.font { + let displayPath = Storage.active.fontURL(forKeyboardID: keyboardID, filename: font) + installableFiles.append([font, displayPath]) + } + do { + for item in installableFiles { + var filePath = folder + if(FileManager.default.fileExists(atPath: (item[1] as! URL).path)) { + try FileManager.default.removeItem(at: item[1] as! URL) } + filePath.appendPathComponent(item[0] as! String) + try FileManager.default.copyItem(at: filePath, + to: item[1] as! URL) + } + } catch { + log.error("Error saving the download: \(error)") + throw KMPError.copyFiles + } + if !haveInstalledOne { + Manager.shared.addKeyboard(keyboard) + haveInstalledOne = true } } - } catch { - log.error("error parsing keyboard kmp: \(error)") - throw KMPError.invalidPackage } } From 3b3f95743d48fa4c5f253944128bd1e628022776 Mon Sep 17 00:00:00 2001 From: jahorton Date: Wed, 3 Jun 2020 09:47:42 +0700 Subject: [PATCH 08/11] refactor(ios/engine): parseLMKMP now relies on KeymanPackage parsing --- .../KMEI/KeymanEngine/Classes/Manager.swift | 115 ++++++------------ 1 file changed, 34 insertions(+), 81 deletions(-) diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Manager.swift b/ios/engine/KMEI/KeymanEngine/Classes/Manager.swift index 9a784887a0..dddcb6eda4 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/Manager.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/Manager.swift @@ -668,90 +668,43 @@ public class Manager: NSObject, UIGestureRecognizerDelegate { // MARK: - Adhoc lexical models static public func parseLMKMP(_ folder: URL, isCustom: Bool) throws -> Void { - do { - var path = folder - path.appendPathComponent("kmp.json") - let data = try Data(contentsOf: path, options: .mappedIfSafe) - let jsonResult = try JSONSerialization.jsonObject(with: data, options: .mutableLeaves) - if let jsonResult = jsonResult as? [String:AnyObject] { - var version: String = "1.0" + guard let kmp = KeymanPackage.parse(folder) as? LexicalModelKeymanPackage else { + throw KMPError.wrongPackageType + } - if let info = jsonResult["info"] as? [String:AnyObject] { - if let versionEntry = info["version"] as? [String:AnyObject] { - if let description = versionEntry["description"] as? String { - version = description; - } - } - } + for m in kmp.models { + let installableLexicalModels : [InstallableLexicalModel] = m.installableLexicalModels + let lexicalModelID = m.lexicalModelId - // Version uses a 'conditional initializer'. If it fails, the version info is invalid. - if let _ = Version(version) { - // No problem - } else { - // Lazy-handle the error and replace version with 1.0. Legacy decision from 2005. - version = "1.0" - } - - if let lexicalModels = jsonResult["lexicalModels"] as? [[String:AnyObject]] { - for k in lexicalModels { - let name = k["name"] as! String - let lexicalModelID = k["id"] as! String - - //TODO: handle errors if languages do not exist - //var languageName = "" - var languageId = "" - - var installableLexicalModels : [InstallableLexicalModel] = [] - if let langs = k["languages"] as? [[String:String]] { - for l in langs { - //languageName = l["name"]! - languageId = l["id"]! - - installableLexicalModels.append( InstallableLexicalModel( - id: lexicalModelID, - name: name, - languageID: languageId, -// languageName: languageName, - version: version, - isCustom: isCustom)) - } - } - - do { - try FileManager.default.createDirectory(at: Storage.active.lexicalModelDir(forID: lexicalModelID), - withIntermediateDirectories: true) - } catch { - log.error("Could not create dir for download: \(error)") - throw KMPError.fileSystem - } - - for lexicalModel in installableLexicalModels { - let storedPath = Storage.active.lexicalModelURL(for: lexicalModel) - - let installableFiles: [[Any]] = [["\(lexicalModelID).model.js", storedPath]] - do { - for item in installableFiles { - var filePath = folder - if(FileManager.default.fileExists(atPath: (item[1] as! URL).path)) { - try FileManager.default.removeItem(at: item[1] as! URL) - } - filePath.appendPathComponent(item[0] as! String) - try FileManager.default.copyItem(at: filePath, - to: item[1] as! URL) - - } - } catch { - log.error("Error saving the lexical model download: \(error)") - throw KMPError.copyFiles - } - Manager.addLexicalModel(lexicalModel) - } - } - } + do { + try FileManager.default.createDirectory(at: Storage.active.lexicalModelDir(forID: lexicalModelID), + withIntermediateDirectories: true) + } catch { + log.error("Could not create dir for download: \(error)") + throw KMPError.fileSystem + } + + for lexicalModel in installableLexicalModels { + let storedPath = Storage.active.lexicalModelURL(for: lexicalModel) + + let installableFiles: [[Any]] = [["\(lexicalModelID).model.js", storedPath]] + do { + for item in installableFiles { + var filePath = folder + if(FileManager.default.fileExists(atPath: (item[1] as! URL).path)) { + try FileManager.default.removeItem(at: item[1] as! URL) + } + filePath.appendPathComponent(item[0] as! String) + try FileManager.default.copyItem(at: filePath, + to: item[1] as! URL) + + } + } catch { + log.error("Error saving the lexical model download: \(error)") + throw KMPError.copyFiles + } + Manager.addLexicalModel(lexicalModel) } - } catch { - log.error("error parsing lexical model kmp: \(error)") - throw KMPError.invalidPackage } } From d2993ed6ef99740ce45828e1ac653abb910de2de Mon Sep 17 00:00:00 2001 From: jahorton Date: Wed, 3 Jun 2020 10:12:07 +0700 Subject: [PATCH 09/11] change(ios/engine): minor polish --- ios/engine/KMEI/KeymanEngine/Classes/KeymanPackage.swift | 3 +-- ios/engine/KMEI/KeymanEngine/Classes/Manager.swift | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/ios/engine/KMEI/KeymanEngine/Classes/KeymanPackage.swift b/ios/engine/KMEI/KeymanEngine/Classes/KeymanPackage.swift index 2f81cecc7c..3625e09acb 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/KeymanPackage.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/KeymanPackage.swift @@ -26,8 +26,7 @@ public class KeymanPackage { sourceFolder = folder self.metadata = metadata } - - // to be overridden by subclasses + public func isKeyboard() -> Bool { return metadata.packageType == .Keyboard } diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Manager.swift b/ios/engine/KMEI/KeymanEngine/Classes/Manager.swift index dddcb6eda4..3e4ae2c00f 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/Manager.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/Manager.swift @@ -697,7 +697,6 @@ public class Manager: NSObject, UIGestureRecognizerDelegate { filePath.appendPathComponent(item[0] as! String) try FileManager.default.copyItem(at: filePath, to: item[1] as! URL) - } } catch { log.error("Error saving the lexical model download: \(error)") From 61e868b769cc884b455de901650ae7444dcebcdd Mon Sep 17 00:00:00 2001 From: jahorton Date: Thu, 4 Jun 2020 08:41:16 +0700 Subject: [PATCH 10/11] fix(ios): missing CodingKeys for kmp.json forward-compat --- .../KMEI/KeymanEngine/Classes/kmp.json/KMPSystem.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ios/engine/KMEI/KeymanEngine/Classes/kmp.json/KMPSystem.swift b/ios/engine/KMEI/KeymanEngine/Classes/kmp.json/KMPSystem.swift index 7b6f46ba21..50a941d9d5 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/kmp.json/KMPSystem.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/kmp.json/KMPSystem.swift @@ -11,4 +11,9 @@ import Foundation public struct KMPSystem: Codable { public var keymanDeveloperVersion: String? public var fileVersion: String + + enum CodingKeys: String, CodingKey { + case keymanDeveloperVersion + case fileVersion + } } From 66fbc3b611e557281b60e62075be9f89afe74c90 Mon Sep 17 00:00:00 2001 From: jahorton Date: Fri, 5 Jun 2020 11:13:08 +0700 Subject: [PATCH 11/11] feat(ios/engine): language tag case-insensitivity with Codable --- .../KMEI/KeymanEngine/Classes/kmp.json/KMPLanguage.swift | 7 +++++++ ios/engine/KMEI/KeymanEngineTests/KMPJSONTests.swift | 7 +++++++ .../KMEI/KeymanEngineTests/TestUtils/PackageJSON.swift | 1 + .../resources/PackageJSON.bundle/language-str-Latn.json | 4 ++++ 4 files changed, 19 insertions(+) create mode 100644 ios/engine/KMEI/KeymanEngineTests/resources/PackageJSON.bundle/language-str-Latn.json diff --git a/ios/engine/KMEI/KeymanEngine/Classes/kmp.json/KMPLanguage.swift b/ios/engine/KMEI/KeymanEngine/Classes/kmp.json/KMPLanguage.swift index 688c2fba81..ae07411e7b 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/kmp.json/KMPLanguage.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/kmp.json/KMPLanguage.swift @@ -16,4 +16,11 @@ public struct KMPLanguage: Codable { case name case languageId = "id" } + + public init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + + name = try values.decode(String.self, forKey: .name) + languageId = try values.decode(String.self, forKey: .languageId).lowercased() + } } diff --git a/ios/engine/KMEI/KeymanEngineTests/KMPJSONTests.swift b/ios/engine/KMEI/KeymanEngineTests/KMPJSONTests.swift index a6964ad8d0..760684276c 100644 --- a/ios/engine/KMEI/KeymanEngineTests/KMPJSONTests.swift +++ b/ios/engine/KMEI/KeymanEngineTests/KMPJSONTests.swift @@ -27,6 +27,13 @@ class KMPJSONTests: XCTestCase { XCTAssertEqual(lang_km.name, "Central Khmer (Khmer, Cambodia)") XCTAssertEqual(lang_km.languageId, "km") + + let lang_str_latn: KMPLanguage = try loadObjectFromJSON(at: TestUtils.PackageJSON.language_str_latn) + + XCTAssertEqual(lang_str_latn.name, "SENĆOŦEN") + // Is upper-cased in the .json, but should be lower-cased on deserialization. + // Our language-tag matching is case-insensitive. + XCTAssertEqual(lang_str_latn.languageId, "str-latn") } // A helper method, since these are useful in two separate tests. diff --git a/ios/engine/KMEI/KeymanEngineTests/TestUtils/PackageJSON.swift b/ios/engine/KMEI/KeymanEngineTests/TestUtils/PackageJSON.swift index 0bcfc5c4df..2e9744a4ec 100644 --- a/ios/engine/KMEI/KeymanEngineTests/TestUtils/PackageJSON.swift +++ b/ios/engine/KMEI/KeymanEngineTests/TestUtils/PackageJSON.swift @@ -15,6 +15,7 @@ extension TestUtils { // place entries here. static let language_en = jsonBundle.url(forResource: "language-en", withExtension: "json")! static let language_km = jsonBundle.url(forResource: "language-km", withExtension: "json")! + static let language_str_latn = jsonBundle.url(forResource: "language-str-Latn", withExtension: "json")! static let keyboard_khmer_angkor = jsonBundle.url(forResource: "keyboard-khmer_angkor", withExtension: "json")! diff --git a/ios/engine/KMEI/KeymanEngineTests/resources/PackageJSON.bundle/language-str-Latn.json b/ios/engine/KMEI/KeymanEngineTests/resources/PackageJSON.bundle/language-str-Latn.json new file mode 100644 index 0000000000..cef9583924 --- /dev/null +++ b/ios/engine/KMEI/KeymanEngineTests/resources/PackageJSON.bundle/language-str-Latn.json @@ -0,0 +1,4 @@ +{ + "name": "SENĆOŦEN", + "id": "str-Latn" +}