diff --git a/Package.swift b/Package.swift index 7ba6627..822a47a 100644 --- a/Package.swift +++ b/Package.swift @@ -5,6 +5,7 @@ import PackageDescription let package = Package( name: "CertLogic", + defaultLocalization: "en", platforms: [ .macOS(.v10_13), .iOS(.v11), .tvOS(.v9), .watchOS(.v2) ], @@ -24,7 +25,8 @@ let package = Package( // Targets can depend on other targets in this package, and on products in packages this package depends on. .target( name: "CertLogic", - dependencies: ["jsonlogic", "SwiftyJSON"]), + dependencies: ["jsonlogic", "SwiftyJSON"], + resources: [.process("Resources")]), .testTarget( name: "CertLogicTests", dependencies: ["CertLogic", "jsonlogic", "SwiftyJSON"]), diff --git a/Resources/en.lproj/Localizible.strings b/Resources/en.lproj/Localizible.strings new file mode 100644 index 0000000..d0e563b --- /dev/null +++ b/Resources/en.lproj/Localizible.strings @@ -0,0 +1,8 @@ +// +// File.swift +// +// +// Created by Alexandr Chernyy on 29.06.2021. +// +"unknown_error" = "An unexpected error occurred"; +"recheck_rule" = "Check Re-open EU for further details and travel-related rules"; diff --git a/Sources/CertLogic/CertLogic.swift b/Sources/CertLogic/CertLogic.swift index 1fd74e7..51c9081 100644 --- a/Sources/CertLogic/CertLogic.swift +++ b/Sources/CertLogic/CertLogic.swift @@ -54,7 +54,7 @@ final public class CertLogicEngine { } rulesItems.forEach { rule in if !checkSchemeVersion(for: rule, qrCodeSchemeVersion: qrCodeSchemeVersion) { - result.append(ValidationResult(rule: rule, result: .open, validationErrors: nil)) + result.append(ValidationResult(rule: rule, result: .open, validationErrors: [CertLogicError.openState])) } else { do { let jsonlogic = try JsonLogic(rule.logic.description) @@ -90,13 +90,13 @@ final public class CertLogicEngine { return true } - // MARK: calculate scheme version in Int "1.0.0" -> 100, "1.2.0" -> 120, 2.0.0 -> 200 + // MARK: calculate scheme version in Int "1.0.0" -> 10000, "1.2.0" -> 10200, 2.0.1 -> 20001 private func getVersion(from schemeString: String) -> Int { let codeVersionItems = schemeString.components(separatedBy: ".") var version: Int = 0 let maxIndex = codeVersionItems.count - 1 for index in 0...maxIndex { - let division = Int(pow(Double(10), Double(Constants.maxVersion - index))) + let division = Int(pow(Double(100), Double(Constants.maxVersion - index))) let calcVersion: Int = Int(codeVersionItems[index]) ?? 1 let forSum: Int = calcVersion * division version = version + forSum @@ -116,9 +116,53 @@ final public class CertLogicEngine { // Get List of Rules for Country by Code private func getListOfRulesFor(external: ExternalParameter, issuerCountryCode: String) -> [Rule] { - return rules.filter { rule in - return rule.countryCode.lowercased() == external.countryCode.lowercased() && rule.ruleType == .acceptence || rule.countryCode.lowercased() == issuerCountryCode.lowercased() && rule.ruleType == .invalidation && rule.certificateFullType == external.certificationType || rule.certificateFullType == .general && external.validationClock >= rule.validFromDate && external.validationClock <= rule.validToDate + var returnedRulesItems: [Rule] = [] + let generalRulesWithAcceptence = rules.filter { rule in + return rule.countryCode.lowercased() == external.countryCode.lowercased() && rule.ruleType == .acceptence && rule.certificateFullType == .general && external.validationClock >= rule.validFromDate && external.validationClock <= rule.validToDate } + let generalRulesWithInvalidation = rules.filter { rule in + return rule.countryCode.lowercased() == issuerCountryCode.lowercased() && rule.ruleType == .invalidation && rule.certificateFullType == .general && external.validationClock >= rule.validFromDate && external.validationClock <= rule.validToDate + } + //General Rule with Acceptence type and max Version number + if generalRulesWithAcceptence.count > 0 { + if let maxRules = generalRulesWithAcceptence.max(by: { (ruleOne, ruleTwo) -> Bool in + return ruleOne.versionInt < ruleTwo.versionInt + }) { + returnedRulesItems.append( maxRules) + } + } + //General Rule with Invalidation type and max Version number + if generalRulesWithInvalidation.count > 0 { + if let maxRules = generalRulesWithInvalidation.max(by: { (ruleOne, ruleTwo) -> Bool in + return ruleOne.versionInt < ruleTwo.versionInt + }) { + returnedRulesItems.append( maxRules) + } + } + let certTypeRulesWithAcceptence = rules.filter { rule in + return rule.countryCode.lowercased() == external.countryCode.lowercased() && rule.ruleType == .acceptence && rule.certificateFullType == external.certificationType && external.validationClock >= rule.validFromDate && external.validationClock <= rule.validToDate + } + let certTypeRulesWithInvalidation = rules.filter { rule in + return rule.countryCode.lowercased() == issuerCountryCode.lowercased() && rule.ruleType == .invalidation && rule.certificateFullType == external.certificationType && external.validationClock >= rule.validFromDate && external.validationClock <= rule.validToDate + } + + //Rule with CertificationType with Acceptence type and max Version number + if certTypeRulesWithAcceptence.count > 0 { + if let maxRules = certTypeRulesWithAcceptence.max(by: { (ruleOne, ruleTwo) -> Bool in + return ruleOne.versionInt < ruleTwo.versionInt + }) { + returnedRulesItems.append( maxRules) + } + } + //Rule with CertificationType with Invalidation type and max Version number + if certTypeRulesWithInvalidation.count > 0 { + if let maxRules = certTypeRulesWithInvalidation.max(by: { (ruleOne, ruleTwo) -> Bool in + return ruleOne.versionInt < ruleTwo.versionInt + }) { + returnedRulesItems.append( maxRules) + } + } + return returnedRulesItems } static public func getItems(from jsonString: String) -> [T] { @@ -143,34 +187,61 @@ final public class CertLogicEngine { public func getDetailsOfError(rule: Rule, external: ExternalParameter) -> String { var value: String = "" rule.affectedString.forEach { key in - var section = "test_entry" - if external.certificationType == .recovery { - section = "recovery_entry" - } - if external.certificationType == .vacctination { - section = "vaccination_entry" + var keyToGetValue: String? = nil + let arrayKeys = key.components(separatedBy: ".") + // For affected fields like "ma" + if arrayKeys.count == 0 { + keyToGetValue = key } - if external.certificationType == .test { - section = "test_entry" + // For affected fields like r.0.fr + if arrayKeys.count == 3 { + keyToGetValue = arrayKeys.last } - if let newValue = schema?["$defs"][section]["properties"][key]["description"].string { - if value.count == 0 { - value = value + "\(newValue)" - } else { - value = value + " / " + "\(newValue)" + // All other keys will skiped (example: "r.0") + if let keyToGetValue = keyToGetValue { + if let newValue = self.getValueFromSchemeBy(external: external, key: keyToGetValue) { + if value.count == 0 { + value = value + "\(newValue)" + } else { + value = value + " / " + "\(newValue)" + } } } } return value } + + private func getValueFromSchemeBy(external: ExternalParameter, key: String) -> String? { + var section = Constants.testEntry + if external.certificationType == .recovery { + section = Constants.recoveryEntry + } + if external.certificationType == .vacctination { + section = Constants.vaccinationEntry + } + if external.certificationType == .test { + section = Constants.testEntry + } + if let newValue = schema?[Constants.schemeDefsSection][section][Constants.properties][key][Constants.description].string { + return newValue + } + return nil + } + } extension CertLogicEngine { - enum Constants { + private enum Constants { static let payload = "payload" static let external = "external" static let defSchemeVersion = "1.0.0" static let maxVersion: Int = 2 - static let majorVersionForSkip: Int = 100 + static let majorVersionForSkip: Int = 10000 + static let testEntry = "test_entry" + static let vaccinationEntry = "vaccination_entry" + static let recoveryEntry = "recovery_entry" + static let schemeDefsSection = "$defs" + static let properties = "properties" + static let description = "description" } } diff --git a/Sources/CertLogic/CertLogicError.swift b/Sources/CertLogic/CertLogicError.swift new file mode 100644 index 0000000..cd9f091 --- /dev/null +++ b/Sources/CertLogic/CertLogicError.swift @@ -0,0 +1,47 @@ +// +// File.swift +// +// +// Created by Alexandr Chernyy on 29.06.2021. +// + +import Foundation + +enum CertLogicError: Error { + // Throw when an schemeversion not valid + case openState + // Throw in all other cases + case unexpected(code: Int) +} + +extension CertLogicError: CustomStringConvertible { + public var description: String { + switch self { + case .openState: + return NSLocalizedString( + "recheck_rule", + comment: "Invalid Password" + ) + case .unexpected(_): + return NSLocalizedString( + "unknown_error", + comment: "Unexpected Error") + } + } +} + +extension CertLogicError: LocalizedError { + public var errorDescription: String? { + switch self { + case .openState: + return NSLocalizedString( + "recheck_rule", + comment: "Invalid Password" + ) + case .unexpected(_): + return NSLocalizedString( + "unknown_error", + comment: "Unexpected Error") + } + } +} diff --git a/Sources/CertLogic/Date+.swift b/Sources/CertLogic/Date+.swift index 6c5a89d..287f9de 100644 --- a/Sources/CertLogic/Date+.swift +++ b/Sources/CertLogic/Date+.swift @@ -26,6 +26,24 @@ extension Date { return formatter }() + static var isoFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" + formatter.calendar = Calendar(identifier: .iso8601) + formatter.timeZone = TimeZone(abbreviation: "UTC") + formatter.locale = Locale(identifier: "en_US_POSIX") + return formatter + }() + + static var isoFormatterNotFull: DateFormatter = { + let formatter = DateFormatter() + formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss'Z'" + formatter.calendar = Calendar(identifier: .iso8601) + formatter.timeZone = TimeZone(abbreviation: "UTC") + formatter.locale = Locale(identifier: "en_US_POSIX") + return formatter + }() + var ISO8601String: String { return Date.iso8601Full.string(from: self) } } diff --git a/Sources/CertLogic/Rule.swift b/Sources/CertLogic/Rule.swift index e5b496e..407ace4 100644 --- a/Sources/CertLogic/Rule.swift +++ b/Sources/CertLogic/Rule.swift @@ -24,7 +24,9 @@ public enum CertificateType: String { fileprivate enum Constants { static let defLanguage = "EN" - static let unknownError = "Unknown error" + static let unknownError = NSLocalizedString("unknown_error", comment: "Packege unknown error") + static let maxVersion = 2 + static let zero = 0 } public class Rule: Codable { @@ -54,20 +56,32 @@ public class Rule: Codable { } public var validFromDate: Date { - get { return Date.backendFormatter.date(from: validFrom) ?? Date() } + get { + if let date = Date.backendFormatter.date(from: validFrom) { return date } + if let date = Date.isoFormatter.date(from: validFrom) { return date } + if let date = Date.iso8601Full.date(from: validFrom) { return date } + if let date = Date.isoFormatterNotFull.date(from: validFrom) { return date } + return Date() + } } public var validToDate: Date { - get { return Date.backendFormatter.date(from: validTo) ?? Date() } + get { + if let date = Date.backendFormatter.date(from: validTo) { return date } + if let date = Date.isoFormatter.date(from: validTo) { return date } + if let date = Date.iso8601Full.date(from: validTo) { return date } + if let date = Date.isoFormatterNotFull.date(from: validTo) { return date } + return Date() + } } public var versionInt: Int { get { let codeVersionItems = version.components(separatedBy: ".") - var version: Int = 0 + var version: Int = Constants.zero let maxIndex = codeVersionItems.count - 1 - for index in 0...maxIndex { - let division = Int(pow(Double(10), Double(2 - index))) + for index in Constants.zero...maxIndex { + let division = Int(pow(Double(100), Double(Constants.maxVersion - index))) let calcVersion: Int = Int(codeVersionItems[index]) ?? 1 let forSum: Int = calcVersion * division version = version + forSum @@ -80,21 +94,21 @@ public class Rule: Codable { let filtered = self.description.filter { description in description.lang.lowercased() == locale.lowercased() } - if(filtered.count == 0) { + if(filtered.count == Constants.zero) { let defFiltered = self.description.filter { description in description.lang.lowercased() == Constants.defLanguage.lowercased() } - if defFiltered.count == 0 { - if self.description.count == 0 { + if defFiltered.count == Constants.zero { + if self.description.count == Constants.zero { return Constants.unknownError } else { - return self.description[0].desc + return self.description[Constants.zero].desc } } else { - return defFiltered[0].desc + return defFiltered[Constants.zero].desc } } else { - return filtered[0].desc + return filtered[Constants.zero].desc } } diff --git a/Tests/CertLogicTests/CertLogicTests.swift b/Tests/CertLogicTests/CertLogicTests.swift index 30e5610..2a2335e 100644 --- a/Tests/CertLogicTests/CertLogicTests.swift +++ b/Tests/CertLogicTests/CertLogicTests.swift @@ -344,8 +344,8 @@ final class CertLogicTests: XCTestCase { "valueSets": { }, "countryCode": "ua", - "exp": "2021-06-14T17:07:36.622", - "iat": "2021-06-14T17:07:36.622" + "exp": "2021-07-14T17:07:36.622", + "iat": "2021-05-14T17:07:36.622" } """ @@ -375,7 +375,7 @@ final class CertLogicTests: XCTestCase { } """ - let jsonString = + let rulesString = """ [ { @@ -393,15 +393,60 @@ final class CertLogicTests: XCTestCase { "desc": "The Field “Doses” MUST contain number 2 OR 2/2." } ], - "ValidFrom": "2021-05-27T07:46:40Z", - "ValidTo": "2021-06-01T07:46:40Z", + "ValidFrom": "2021-05-27T00:00:00Z", + "ValidTo": "2021-08-01T00:00:00Z", "AffectedFields": [ "dt", "nm" ], "Logic": {"and":[{">=":[{"var":"dt"},"23.12.2012"]},{">=":[{"var":"nm"},"ABC"]}]} }, - { + { + "Identifier": "GR-CZ-0001", + "Version": "1.0.0", + "SchemaVersion": "1.0.0", + "Engine": "CERTLOGIC", + "EngineVersion": "2.0.1", + "Type": "Acceptance", + "CertificateType": "General", + "Country": "ua", + "Description": [ + { + "lang": "en", + "desc": "The Field “Doses” MUST contain number 2 OR 2/2." + } + ], + "ValidFrom": "2021-05-27T00:00:00Z", + "ValidTo": "2021-08-01T00:00:00Z", + "AffectedFields": [ + "dt", + "nm" + ], + "Logic": {"and":[{">=":[{"var":"dt"},"23.12.2012"]},{">=":[{"var":"nm"},"ABC"]}]} + }, + { + "Identifier": "GR-CZ-0001", + "Version": "1.1.0", + "SchemaVersion": "1.0.0", + "Engine": "CERTLOGIC", + "EngineVersion": "2.0.1", + "Type": "Acceptance", + "CertificateType": "General", + "Country": "ua", + "Description": [ + { + "lang": "en", + "desc": "The Field “Doses” MUST contain number 2 OR 2/2." + } + ], + "ValidFrom": "2021-05-27T00:00:00Z", + "ValidTo": "2021-08-01T00:00:00Z", + "AffectedFields": [ + "dt", + "nm" + ], + "Logic": {"and":[{">=":[{"var":"dt"},"23.12.2012"]},{">=":[{"var":"nm"},"ABC"]}]} + },{ "Identifier": "GR-UA-0002", "Version": "1.0.0", "SchemaVersion": "1.0.0", @@ -416,8 +461,8 @@ final class CertLogicTests: XCTestCase { "desc": "The Field “Doses” MUST contain number 2 OR 2/2." } ], - "ValidFrom": "2021-05-27T07:46:40Z", - "ValidTo": "2021-06-01T07:46:40Z", + "ValidFrom": "2021-05-27T00:00:00Z", + "ValidTo": "2021-08-01T0T00:00:00Z", "AffectedFields": [ "dt", "nm" @@ -439,8 +484,8 @@ final class CertLogicTests: XCTestCase { "desc": "The Field “Doses” MUST contain number 2 OR 2/2." } ], - "ValidFrom": "2021-05-27T07:46:40Z", - "ValidTo": "2021-06-01T07:46:40Z", + "ValidFrom": "2021-05-01T00:00:00Z", + "ValidTo": "2030-06-01T00:00:00Z", "AffectedFields": [ "dt", "nm" @@ -462,8 +507,8 @@ final class CertLogicTests: XCTestCase { "desc": "The Field “Doses” MUST contain number 2 OR 2/2." } ], - "ValidFrom": "2021-05-27T07:46:40Z", - "ValidTo": "2021-06-01T07:46:40Z", + "ValidFrom": "2021-05-01T00:00:00Z", + "ValidTo": "2030-06-01T00:00:00Z", "AffectedFields": [ "dt", "nm" @@ -571,7 +616,7 @@ final class CertLogicTests: XCTestCase { if let externalParameter: ExternalParameter = CertLogicEngine.getItem(from: external) { - let rules: [Rule] = CertLogicEngine.getItems(from: jsonString) + let rules: [Rule] = CertLogicEngine.getItems(from: rulesString) let valueSet: ValueSet? = CertLogicEngine.getItem(from: valueSetString) let engine = CertLogicEngine(schema: euDgcSchemaV1, rules: rules) let result = engine.validate(external: externalParameter, payload: payload)