JSONCompare.swift 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  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. public 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. public 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. public struct ValueDifference: Codable {
  45. let js: JSONValue
  46. let native: JSONValue
  47. let jsKeyMissing: Bool
  48. let nativeKeyMissing: Bool
  49. }
  50. public enum JSONCompare {
  51. public static func logDifferences(
  52. label: String,
  53. native: String,
  54. nativeRuntime: TimeInterval,
  55. javascript: String,
  56. javascriptRuntime: TimeInterval
  57. ) {
  58. guard let differences = try? differences(native: native, javascript: javascript) else {
  59. warning(.openAPS, "Exception calculating differences")
  60. return
  61. }
  62. // TODO: For now we'll just print this out to the console but we'll add proper logging next
  63. debug(.openAPS, "\(label) -> n: \(nativeRuntime)s, js: \(javascriptRuntime)s")
  64. prettyPrint(differences)
  65. }
  66. public static func prettyPrint(_ differences: [String: ValueDifference]) {
  67. let encoder = JSONEncoder()
  68. encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
  69. if let data = try? encoder.encode(differences),
  70. let prettyString = String(data: data, encoding: .utf8)
  71. {
  72. debug(.openAPS, prettyString)
  73. }
  74. }
  75. public static func differences(native: String, javascript: String) throws -> [String: ValueDifference] {
  76. guard let jsData = javascript.data(using: .utf8),
  77. let nativeData = native.data(using: .utf8),
  78. let jsDict = try JSONSerialization.jsonObject(with: jsData) as? [String: Any],
  79. let nativeDict = try JSONSerialization.jsonObject(with: nativeData) as? [String: Any]
  80. else {
  81. throw NSError(domain: "JSONBridge", code: 1, userInfo: [NSLocalizedDescriptionKey: "Invalid JSON format"])
  82. }
  83. var differences: [String: ValueDifference] = [:]
  84. // Check all keys present in either dictionary
  85. Set(jsDict.keys).union(nativeDict.keys).forEach { key in
  86. let jsValue = jsDict[key].map(convertToJSONValue) ?? .null
  87. let nativeValue = nativeDict[key].map(convertToJSONValue) ?? .null
  88. if !valuesAreEqual(jsValue, nativeValue) {
  89. differences[key] = ValueDifference(
  90. js: jsValue,
  91. native: nativeValue,
  92. jsKeyMissing: !jsDict.keys.contains(key),
  93. nativeKeyMissing: !nativeDict.keys.contains(key)
  94. )
  95. }
  96. }
  97. return differences
  98. }
  99. private static func convertToJSONValue(_ value: Any) -> JSONValue {
  100. switch value {
  101. case let string as String:
  102. return .string(string)
  103. case let number as NSNumber:
  104. return .number(number.doubleValue)
  105. case let bool as Bool:
  106. return .boolean(bool)
  107. case let array as [Any]:
  108. return .array(array.map(convertToJSONValue))
  109. case let dict as [String: Any]:
  110. return .object(dict.mapValues(convertToJSONValue))
  111. case is NSNull:
  112. return .null
  113. default:
  114. return .null
  115. }
  116. }
  117. private static func valuesAreEqual(_ value1: JSONValue, _ value2: JSONValue) -> Bool {
  118. switch (value1, value2) {
  119. case (.null, .null):
  120. return true
  121. case let (.string(s1), .string(s2)):
  122. return s1 == s2
  123. case let (.number(n1), .number(n2)):
  124. return n1 == n2
  125. case let (.boolean(b1), .boolean(b2)):
  126. return b1 == b2
  127. case let (.array(a1), .array(a2)):
  128. return a1.count == a2.count && zip(a1, a2).allSatisfy(valuesAreEqual)
  129. case let (.object(o1), .object(o2)):
  130. return o1.keys == o2.keys && o1.keys.allSatisfy { key in
  131. guard let v1 = o1[key], let v2 = o2[key] else { return false }
  132. return valuesAreEqual(v1, v2)
  133. }
  134. default:
  135. return false
  136. }
  137. }
  138. }