| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171 |
- import Foundation
- enum JSONValue: Codable {
- case string(String)
- case number(Double)
- case boolean(Bool)
- case array([JSONValue])
- case object([String: JSONValue])
- case null
- init(from decoder: Decoder) throws {
- let container = try decoder.singleValueContainer()
- if container.decodeNil() {
- self = .null
- return
- }
- if let string = try? container.decode(String.self) {
- self = .string(string)
- } else if let number = try? container.decode(Double.self) {
- self = .number(number)
- } else if let boolean = try? container.decode(Bool.self) {
- self = .boolean(boolean)
- } else if let array = try? container.decode([JSONValue].self) {
- self = .array(array)
- } else if let object = try? container.decode([String: JSONValue].self) {
- self = .object(object)
- } else {
- throw DecodingError.typeMismatch(JSONValue.self, DecodingError.Context(
- codingPath: decoder.codingPath,
- debugDescription: "Invalid JSON value"
- ))
- }
- }
- func encode(to encoder: Encoder) throws {
- var container = encoder.singleValueContainer()
- switch self {
- case let .string(value): try container.encode(value)
- case let .number(value): try container.encode(value)
- case let .boolean(value): try container.encode(value)
- case let .array(value): try container.encode(value)
- case let .object(value): try container.encode(value)
- case .null: try container.encodeNil()
- }
- }
- }
- struct ValueDifference: Codable {
- let js: JSONValue
- let native: JSONValue
- let jsKeyMissing: Bool
- let nativeKeyMissing: Bool
- }
- enum JSONCompare {
- enum Function {
- case makeProfile
- // since we're removing some keys from our Profile that exist in Javascript
- // we need to let the difference function know which keys to ignore when
- // calculating differences
- func keysToIgnore() -> Set<String> {
- switch self {
- case .makeProfile:
- return Set(["calc_glucose_noise", "enableEnliteBgproxy", "exercise_mode", "offline_hotspot"])
- }
- }
- }
- static func logDifferences(
- function: Function,
- native: String,
- nativeRuntime: TimeInterval,
- javascript: String,
- javascriptRuntime: TimeInterval
- ) {
- guard let differences = try? differences(function: function, native: native, javascript: javascript) else {
- warning(.openAPS, "Exception calculating differences")
- return
- }
- // TODO: For now we'll just print this out to the console but we'll add proper logging next
- debug(.openAPS, "\(function) -> n: \(nativeRuntime)s, js: \(javascriptRuntime)s")
- prettyPrint(differences)
- }
- static func prettyPrint(_ differences: [String: ValueDifference]) {
- let encoder = JSONEncoder()
- encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
- if let data = try? encoder.encode(differences),
- let prettyString = String(data: data, encoding: .utf8)
- {
- debug(.openAPS, prettyString)
- }
- }
- static func differences(function: Function, native: String, javascript: String) throws -> [String: ValueDifference] {
- guard let jsData = javascript.data(using: .utf8),
- let nativeData = native.data(using: .utf8),
- let jsDict = try JSONSerialization.jsonObject(with: jsData) as? [String: Any],
- let nativeDict = try JSONSerialization.jsonObject(with: nativeData) as? [String: Any]
- else {
- throw NSError(domain: "JSONBridge", code: 1, userInfo: [NSLocalizedDescriptionKey: "Invalid JSON format"])
- }
- var differences: [String: ValueDifference] = [:]
- // Check all keys present in either dictionary
- Set(jsDict.keys).union(nativeDict.keys).forEach { key in
- let jsValue = jsDict[key].map(convertToJSONValue) ?? .null
- let nativeValue = nativeDict[key].map(convertToJSONValue) ?? .null
- if !valuesAreEqual(jsValue, nativeValue) {
- differences[key] = ValueDifference(
- js: jsValue,
- native: nativeValue,
- jsKeyMissing: !jsDict.keys.contains(key),
- nativeKeyMissing: !nativeDict.keys.contains(key)
- )
- }
- }
- let keysToIgnore = function.keysToIgnore()
- return differences.filter { !keysToIgnore.contains($0.key) }
- }
- private static func convertToJSONValue(_ value: Any) -> JSONValue {
- switch value {
- case let string as String:
- return .string(string)
- case let number as NSNumber:
- return .number(number.doubleValue)
- case let bool as Bool:
- return .boolean(bool)
- case let array as [Any]:
- return .array(array.map(convertToJSONValue))
- case let dict as [String: Any]:
- return .object(dict.mapValues(convertToJSONValue))
- case is NSNull:
- return .null
- default:
- return .null
- }
- }
- private static func valuesAreEqual(_ value1: JSONValue, _ value2: JSONValue) -> Bool {
- switch (value1, value2) {
- case (.null, .null):
- return true
- case let (.string(s1), .string(s2)):
- return s1 == s2
- case let (.number(n1), .number(n2)):
- return n1 == n2
- case let (.boolean(b1), .boolean(b2)):
- return b1 == b2
- case let (.array(a1), .array(a2)):
- return a1.count == a2.count && zip(a1, a2).allSatisfy(valuesAreEqual)
- case let (.object(o1), .object(o2)):
- return o1.keys == o2.keys && o1.keys.allSatisfy { key in
- guard let v1 = o1[key], let v2 = o2[key] else { return false }
- return valuesAreEqual(v1, v2)
- }
- default:
- return false
- }
- }
- }
|