diff --git a/ios/engine/KMEI/KeymanEngine.xcodeproj/project.pbxproj b/ios/engine/KMEI/KeymanEngine.xcodeproj/project.pbxproj index 5a5af1d7ac..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 */, @@ -760,9 +763,6 @@ C042ED5C1FC6A65A001D82F4 /* Version.swift */, C08E69901FDA6F6F0026056B /* FullKeyboardID.swift */, 9A079E42223B61AE00581263 /* FullLexicalModelID.swift */, - 1645D5942036C6FF0076C51B /* KeymanPackage.swift */, - 9A079E39223AFF3C00581263 /* KeyboardKeymanPackage.swift */, - 9A079DC9222E050E00581263 /* LexicalModelKeymanPackage.swift */, ); path = Model; sourceTree = ""; @@ -878,6 +878,9 @@ CE973D7F2484A07300F66045 /* KMP Packaging */ = { isa = PBXGroup; children = ( + 1645D5942036C6FF0076C51B /* KeymanPackage.swift */, + 9A079E39223AFF3C00581263 /* KeyboardKeymanPackage.swift */, + 9A079DC9222E050E00581263 /* LexicalModelKeymanPackage.swift */, CE867FF22485F38300B2EAED /* kmp.json */, ); name = "KMP Packaging"; @@ -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/KeymanEngine/Classes/KeyboardKeymanPackage.swift b/ios/engine/KMEI/KeymanEngine/Classes/KeyboardKeymanPackage.swift new file mode 100644 index 0000000000..61ef2101f1 --- /dev/null +++ b/ios/engine/KMEI/KeymanEngine/Classes/KeyboardKeymanPackage.swift @@ -0,0 +1,37 @@ +// +// KeyboardKeymanPackage.swift +// KeymanEngine +// +// Created by Randy Boring on 3/14/19. +// Copyright © 2019 SIL International. All rights reserved. +// + +import Foundation + +public class KeyboardKeymanPackage : KeymanPackage { + public var keyboards: [KMPKeyboard]! + + override init(metadata: KMPMetadata, folder: URL) { + super.init(metadata: metadata, folder: folder) + self.keyboards = [] + + 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) + } else { + log.debug("\(keyboard.name) not valid / corresponding file not found") + } + } + } + } + + public override func defaultInfoHtml() -> String { + var str = "Found Keyboards in package:
" + for keyboard in keyboards { + str += keyboard.keyboardId + "
" + } + return str + } +} + diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Model/KeymanPackage.swift b/ios/engine/KMEI/KeymanEngine/Classes/KeymanPackage.swift similarity index 56% rename from ios/engine/KMEI/KeymanEngine/Classes/Model/KeymanPackage.swift rename to ios/engine/KMEI/KeymanEngine/Classes/KeymanPackage.swift index 7235064674..3625e09acb 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/Model/KeymanPackage.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/KeymanPackage.swift @@ -10,30 +10,27 @@ 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." + case wrongPackageType = "Package provided does not match the type expected by this method" } -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 - } - - // to be overridden by subclasses - public func isKeyboard() -> Bool { - return false + self.metadata = metadata } - // to be overridden by subclasses - public func parse(json: [String:AnyObject], version: String) { - return + public func isKeyboard() -> Bool { + return metadata.packageType == .Keyboard } - + // 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 new file mode 100644 index 0000000000..85e7df6e98 --- /dev/null +++ b/ios/engine/KMEI/KeymanEngine/Classes/LexicalModelKeymanPackage.swift @@ -0,0 +1,40 @@ +// +// LexicalModelKeymanPackage.swift +// KeymanEngine +// +// Created by Randy Boring on 3/4/19. +// Copyright © 2019 SIL International. All rights reserved. +// + +import Foundation + +public class LexicalModelKeymanPackage : KeymanPackage { + public var models : [KMPLexicalModel]! + + override init(metadata: KMPMetadata, folder: URL) { + super.init(metadata: metadata, folder: folder) + self.models = [] + + 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 { + log.debug("\(model.name) not valid / corresponding file not found") + } + } + } + } + + 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/KeymanEngine/Classes/Manager.swift b/ios/engine/KMEI/KeymanEngine/Classes/Manager.swift index 640680f039..3e4ae2c00f 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/Manager.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/Manager.swift @@ -613,191 +613,97 @@ 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 } } // 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 } } diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Model/KeyboardKeymanPackage.swift b/ios/engine/KMEI/KeymanEngine/Classes/Model/KeyboardKeymanPackage.swift deleted file mode 100644 index b3cafe0c76..0000000000 --- a/ios/engine/KMEI/KeymanEngine/Classes/Model/KeyboardKeymanPackage.swift +++ /dev/null @@ -1,54 +0,0 @@ -// -// KeyboardKeymanPackage.swift -// KeymanEngine -// -// Created by Randy Boring on 3/14/19. -// Copyright © 2019 SIL International. All rights reserved. -// - -import Foundation - -public class KeyboardKeymanPackage : KeymanPackage -{ - private var keyboards: [KMPKeyboard]! - - public override func parse(json: [String:AnyObject], version: String) { - 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) - } else { - log.debug("\(keyboard.name) not valid / corresponding file not found") - } - } catch { - // Simply... don't append a keyboard, like with the .isValid check above. - // Denotes original behavior of this method; isn't exactly optimal. - log.debug("Error occurred when processing keyboards: \(String(describing: error))") - continue - } - } - } - } - - public override func defaultInfoHtml() -> String { - var str = "Found Keyboards in package:
" - for keyboard in keyboards { - str += keyboard.keyboardId + "
" - } - return str - } - - public override func isKeyboard() -> Bool { - return true - } - -} - diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Model/LexicalModelKeymanPackage.swift b/ios/engine/KMEI/KeymanEngine/Classes/Model/LexicalModelKeymanPackage.swift deleted file mode 100644 index 8fd5004978..0000000000 --- a/ios/engine/KMEI/KeymanEngine/Classes/Model/LexicalModelKeymanPackage.swift +++ /dev/null @@ -1,52 +0,0 @@ -// -// LexicalModelKeymanPackage.swift -// KeymanEngine -// -// Created by Randy Boring on 3/4/19. -// Copyright © 2019 SIL International. All rights reserved. -// - -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 - } - - // may be called parse() - // returns a dictionary mapping language ID to lexical model ID - override public func parse(json: [String:AnyObject], version: String) { - 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) - } else { - log.debug("\(model.name) not valid / corresponding file not found") - } - } catch { - // Append no models. Not the greatest strategy, but it's the one that had always - // been taken here. - log.debug("Error occurred when processing lexical models: \(String(describing: error))") - continue - } - } - } - } - -} - 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/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/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/KeymanEngine/Classes/kmp.json/KMPMetadata.swift b/ios/engine/KMEI/KeymanEngine/Classes/kmp.json/KMPMetadata.swift index 4874822bd7..96debb7c8b 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/kmp.json/KMPMetadata.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/kmp.json/KMPMetadata.swift @@ -65,4 +65,8 @@ public class KMPMetadata: Codable { return .Unsupported } } + + var version: String? { + return self.info?.version?.description + } } 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 + } } diff --git a/ios/engine/KMEI/KeymanEngineTests/FileManagementTests.swift b/ios/engine/KMEI/KeymanEngineTests/FileManagementTests.swift index b760cfe4f0..59efcb087d 100644 --- a/ios/engine/KMEI/KeymanEngineTests/FileManagementTests.swift +++ b/ios/engine/KMEI/KeymanEngineTests/FileManagementTests.swift @@ -21,61 +21,11 @@ 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") 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") @@ -97,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") diff --git a/ios/engine/KMEI/KeymanEngineTests/KMPJSONTests.swift b/ios/engine/KMEI/KeymanEngineTests/KMPJSONTests.swift index 9659922e9d..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. @@ -49,12 +56,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) @@ -68,9 +75,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,15 +100,18 @@ 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, 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) @@ -111,7 +119,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 +129,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/KeymanPackageTests.swift b/ios/engine/KMEI/KeymanEngineTests/KeymanPackageTests.swift new file mode 100644 index 0000000000..8a5046bcc4 --- /dev/null +++ b/ios/engine/KMEI/KeymanEngineTests/KeymanPackageTests.swift @@ -0,0 +1,77 @@ +// +// 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() + + // 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!") + + // 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)") + } + } +} 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/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/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": [ 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" +}