Merge pull request #3205 from keymanapp/refactor/ios/engine/kmp-parsing-and-installation

refactor(ios/engine): KMP installation now relies on KeymanPackage class, parses
This commit is contained in:
Joshua Horton 2020-06-05 12:11:39 +07:00 • committed by GitHub
commit 9395cc2f75
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 333 additions and 391 deletions

View file

@ -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 = "<group>"; };
CE867FF02485EE1500B2EAED /* KMPInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KMPInfo.swift; sourceTree = "<group>"; };
CE88042E247F4B97009BCA1A /* ExpectationDownloaderDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpectationDownloaderDelegate.swift; sourceTree = "<group>"; };
CE8B0BBC248734240045EB2E /* KeymanPackageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeymanPackageTests.swift; sourceTree = "<group>"; };
CE8EDEB223F53F96009E1FF6 /* VersionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionTests.swift; sourceTree = "<group>"; };
CE973D802484A56500F66045 /* PackageJSON.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PackageJSON.swift; sourceTree = "<group>"; };
CE973D822484A5DB00F66045 /* PackageJSON.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = PackageJSON.bundle; sourceTree = "<group>"; };
@ -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 = "<group>";
@ -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 */,

View file

@ -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:<br/>"
for keyboard in keyboards {
str += keyboard.keyboardId + "<br/>"
}
return str
}
}

View file

@ -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 {

View file

@ -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:<br/>"
for model in models {
str += model.lexicalModelId + "<br/>"
}
return str
}
}

View file

@ -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
}
}

View file

@ -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:<br/>"
for keyboard in keyboards {
str += keyboard.keyboardId + "<br/>"
}
return str
}
public override func isKeyboard() -> Bool {
return true
}
}

View file

@ -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:<br/>"
for model in models {
str += model.lexicalModelId + "<br/>"
}
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
}
}
}
}
}

View file

@ -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) {

View file

@ -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? {

View file

@ -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()
}
}

View file

@ -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 {

View file

@ -65,4 +65,8 @@ public class KMPMetadata: Codable {
return .Unsupported
}
}
var version: String? {
return self.info?.version?.description
}
}

View file

@ -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
}
}

View file

@ -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")

View file

@ -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)
}
}

View file

@ -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)")
}
}
}

View file

@ -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)
}
}

View file

@ -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")!

View file

@ -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": [

View file

@ -0,0 +1,4 @@
{
"name": "SENĆOŦEN",
"id": "str-Latn"
}