JSONCompare.swift 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  1. import Foundation
  2. enum JSONValue: Codable {
  3. case string(String)
  4. case number(Double)
  5. case boolean(Bool)
  6. case array([JSONValue])
  7. case object([String: JSONValue])
  8. case null
  9. init(from decoder: Decoder) throws {
  10. let container = try decoder.singleValueContainer()
  11. if container.decodeNil() {
  12. self = .null
  13. return
  14. }
  15. if let string = try? container.decode(String.self) {
  16. self = .string(string)
  17. } else if let number = try? container.decode(Double.self) {
  18. self = .number(number)
  19. } else if let boolean = try? container.decode(Bool.self) {
  20. self = .boolean(boolean)
  21. } else if let array = try? container.decode([JSONValue].self) {
  22. self = .array(array)
  23. } else if let object = try? container.decode([String: JSONValue].self) {
  24. self = .object(object)
  25. } else {
  26. throw DecodingError.typeMismatch(JSONValue.self, DecodingError.Context(
  27. codingPath: decoder.codingPath,
  28. debugDescription: "Invalid JSON value"
  29. ))
  30. }
  31. }
  32. func encode(to encoder: Encoder) throws {
  33. var container = encoder.singleValueContainer()
  34. switch self {
  35. case let .string(value): try container.encode(value)
  36. case let .number(value): try container.encode(value)
  37. case let .boolean(value): try container.encode(value)
  38. case let .array(value): try container.encode(value)
  39. case let .object(value): try container.encode(value)
  40. case .null: try container.encodeNil()
  41. }
  42. }
  43. }
  44. struct ValueDifference: Codable {
  45. let js: JSONValue
  46. let native: JSONValue
  47. let jsKeyMissing: Bool
  48. let nativeKeyMissing: Bool
  49. }
  50. enum JSONCompare {
  51. enum Function {
  52. case makeProfile
  53. // since we're removing some keys from our Profile that exist in Javascript
  54. // we need to let the difference function know which keys to ignore when
  55. // calculating differences
  56. func keysToIgnore() -> Set<String> {
  57. switch self {
  58. case .makeProfile:
  59. return Set(["calc_glucose_noise", "enableEnliteBgproxy", "exercise_mode", "offline_hotspot"])
  60. }
  61. }
  62. }
  63. static func logDifferences(
  64. function: Function,
  65. native: String,
  66. nativeRuntime: TimeInterval,
  67. javascript: String,
  68. javascriptRuntime: TimeInterval
  69. ) {
  70. guard let differences = try? differences(function: function, native: native, javascript: javascript) else {
  71. warning(.openAPS, "Exception calculating differences")
  72. return
  73. }
  74. // TODO: For now we'll just print this out to the console but we'll add proper logging next
  75. debug(.openAPS, "\(function) -> n: \(nativeRuntime)s, js: \(javascriptRuntime)s")
  76. prettyPrint(differences)
  77. }
  78. static func prettyPrint(_ differences: [String: ValueDifference]) {
  79. let encoder = JSONEncoder()
  80. encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
  81. if let data = try? encoder.encode(differences),
  82. let prettyString = String(data: data, encoding: .utf8)
  83. {
  84. debug(.openAPS, prettyString)
  85. }
  86. }
  87. static func differences(function: Function, native: String, javascript: String) throws -> [String: ValueDifference] {
  88. guard let jsData = javascript.data(using: .utf8),
  89. let nativeData = native.data(using: .utf8),
  90. let jsDict = try JSONSerialization.jsonObject(with: jsData) as? [String: Any],
  91. let nativeDict = try JSONSerialization.jsonObject(with: nativeData) as? [String: Any]
  92. else {
  93. throw NSError(domain: "JSONBridge", code: 1, userInfo: [NSLocalizedDescriptionKey: "Invalid JSON format"])
  94. }
  95. var differences: [String: ValueDifference] = [:]
  96. // Check all keys present in either dictionary
  97. Set(jsDict.keys).union(nativeDict.keys).forEach { key in
  98. let jsValue = jsDict[key].map(convertToJSONValue) ?? .null
  99. let nativeValue = nativeDict[key].map(convertToJSONValue) ?? .null
  100. if !valuesAreEqual(jsValue, nativeValue) {
  101. differences[key] = ValueDifference(
  102. js: jsValue,
  103. native: nativeValue,
  104. jsKeyMissing: !jsDict.keys.contains(key),
  105. nativeKeyMissing: !nativeDict.keys.contains(key)
  106. )
  107. }
  108. }
  109. let keysToIgnore = function.keysToIgnore()
  110. return differences.filter { !keysToIgnore.contains($0.key) }
  111. }
  112. private static func convertToJSONValue(_ value: Any) -> JSONValue {
  113. switch value {
  114. case let string as String:
  115. return .string(string)
  116. case let number as NSNumber:
  117. return .number(number.doubleValue)
  118. case let bool as Bool:
  119. return .boolean(bool)
  120. case let array as [Any]:
  121. return .array(array.map(convertToJSONValue))
  122. case let dict as [String: Any]:
  123. return .object(dict.mapValues(convertToJSONValue))
  124. case is NSNull:
  125. return .null
  126. default:
  127. return .null
  128. }
  129. }
  130. private static func valuesAreEqual(_ value1: JSONValue, _ value2: JSONValue) -> Bool {
  131. switch (value1, value2) {
  132. case (.null, .null):
  133. return true
  134. case let (.string(s1), .string(s2)):
  135. return s1 == s2
  136. case let (.number(n1), .number(n2)):
  137. return n1 == n2
  138. case let (.boolean(b1), .boolean(b2)):
  139. return b1 == b2
  140. case let (.array(a1), .array(a2)):
  141. return a1.count == a2.count && zip(a1, a2).allSatisfy(valuesAreEqual)
  142. case let (.object(o1), .object(o2)):
  143. return o1.keys == o2.keys && o1.keys.allSatisfy { key in
  144. guard let v1 = o1[key], let v2 = o2[key] else { return false }
  145. return valuesAreEqual(v1, v2)
  146. }
  147. default:
  148. return false
  149. }
  150. }
  151. }