JSONCompare.swift 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313
  1. import Foundation
  2. /// After the port from Javascript to Swift is complete, we should remove the logging module:
  3. /// https://github.com/nightscout/Trio-dev/issues/293
  4. enum JSONValue: Codable, Equatable {
  5. case string(String)
  6. case number(Double)
  7. case boolean(Bool)
  8. case array([JSONValue])
  9. case object([String: JSONValue])
  10. case null
  11. init(from decoder: Decoder) throws {
  12. let container = try decoder.singleValueContainer()
  13. if container.decodeNil() {
  14. self = .null
  15. return
  16. }
  17. if let string = try? container.decode(String.self) {
  18. self = .string(string)
  19. } else if let number = try? container.decode(Double.self) {
  20. self = .number(number)
  21. } else if let boolean = try? container.decode(Bool.self) {
  22. self = .boolean(boolean)
  23. } else if let array = try? container.decode([JSONValue].self) {
  24. self = .array(array)
  25. } else if let object = try? container.decode([String: JSONValue].self) {
  26. self = .object(object)
  27. } else {
  28. throw DecodingError.typeMismatch(JSONValue.self, DecodingError.Context(
  29. codingPath: decoder.codingPath,
  30. debugDescription: "Invalid JSON value"
  31. ))
  32. }
  33. }
  34. func encode(to encoder: Encoder) throws {
  35. var container = encoder.singleValueContainer()
  36. switch self {
  37. case let .string(value): try container.encode(value)
  38. case let .number(value): try container.encode(value)
  39. case let .boolean(value): try container.encode(value)
  40. case let .array(value): try container.encode(value)
  41. case let .object(value): try container.encode(value)
  42. case .null: try container.encodeNil()
  43. }
  44. }
  45. static func == (lhs: JSONValue, rhs: JSONValue) -> Bool {
  46. switch (lhs, rhs) {
  47. case (.null, .null):
  48. return true
  49. case let (.string(lhs), .string(rhs)):
  50. return lhs == rhs
  51. case let (.number(lhs), .number(rhs)):
  52. return lhs == rhs
  53. case let (.boolean(lhs), .boolean(rhs)):
  54. return lhs == rhs
  55. case let (.array(lhs), .array(rhs)):
  56. return lhs == rhs
  57. case let (.object(lhs), .object(rhs)):
  58. return lhs == rhs
  59. default:
  60. return false
  61. }
  62. }
  63. }
  64. struct ValueDifference: Codable {
  65. let js: JSONValue
  66. let swift: JSONValue
  67. let jsKeyMissing: Bool
  68. let nativeKeyMissing: Bool
  69. }
  70. enum JSONCompare {
  71. static let log = try? JsSwiftOrefComparisonLogger()
  72. static func logDifferences(
  73. function: OrefFunction,
  74. swift: OrefFunctionResult,
  75. swiftDuration: TimeInterval,
  76. javascript: OrefFunctionResult,
  77. javascriptDuration: TimeInterval,
  78. iobInputs: IobInputs? = nil
  79. ) {
  80. let comparison = createComparison(
  81. function: function,
  82. swift: swift,
  83. swiftDuration: swiftDuration,
  84. javascript: javascript,
  85. javascriptDuration: javascriptDuration,
  86. iobInputs: iobInputs
  87. )
  88. Task {
  89. do {
  90. try await log?.logComparison(comparison: comparison)
  91. } catch {
  92. warning(.openAPS, "logComparison exception: \(error)", error: error)
  93. }
  94. }
  95. }
  96. static func createComparison(
  97. function: OrefFunction,
  98. swift: OrefFunctionResult,
  99. swiftDuration: TimeInterval,
  100. javascript: OrefFunctionResult,
  101. javascriptDuration: TimeInterval,
  102. iobInputs: IobInputs?
  103. ) -> AlgorithmComparison {
  104. switch (swift, javascript) {
  105. case let (.success(swiftJson), .success(javascriptJson)):
  106. do {
  107. let differences = try differences(function: function, swift: swiftJson, javascript: javascriptJson)
  108. let resultType: ComparisonResultType = differences.isEmpty ? .matching : .valueDifference
  109. return AlgorithmComparison(
  110. function: function,
  111. resultType: resultType,
  112. jsDuration: javascriptDuration,
  113. swiftDuration: swiftDuration,
  114. differences: differences.isEmpty ? nil : differences,
  115. iobInputs: differences.isEmpty ? nil : iobInputs
  116. )
  117. } catch {
  118. return AlgorithmComparison(
  119. function: function,
  120. resultType: .comparisonError,
  121. jsDuration: javascriptDuration,
  122. swiftDuration: swiftDuration,
  123. comparisonError: AlgorithmException(error: error)
  124. )
  125. }
  126. case let (.failure(swiftError), .failure(jsError)):
  127. return AlgorithmComparison(
  128. function: function,
  129. resultType: .matchingExceptions,
  130. jsException: AlgorithmException(error: jsError),
  131. swiftException: AlgorithmException(error: swiftError)
  132. )
  133. case let (.failure(swiftError), .success):
  134. return AlgorithmComparison(
  135. function: function,
  136. resultType: .swiftOnlyException,
  137. jsDuration: javascriptDuration,
  138. swiftException: AlgorithmException(error: swiftError),
  139. iobInputs: iobInputs
  140. )
  141. case let (.success, .failure(jsError)):
  142. return AlgorithmComparison(
  143. function: function,
  144. resultType: .jsOnlyException,
  145. swiftDuration: swiftDuration,
  146. jsException: AlgorithmException(error: jsError),
  147. iobInputs: iobInputs
  148. )
  149. }
  150. }
  151. static func prettyPrint(_ differences: [String: ValueDifference]) {
  152. let encoder = JSONEncoder()
  153. encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
  154. if let data = try? encoder.encode(differences),
  155. let prettyString = String(data: data, encoding: .utf8)
  156. {
  157. debug(.openAPS, prettyString)
  158. }
  159. }
  160. static func differences(function: OrefFunction, swift: String, javascript: String) throws -> [String: ValueDifference] {
  161. let differences = try {
  162. switch function.returnType() {
  163. case .array:
  164. return try differencesArray(function: function, swift: swift, javascript: javascript)
  165. case .dictionary:
  166. return try differencesDictionary(function: function, swift: swift, javascript: javascript)
  167. }
  168. }()
  169. let keysToIgnore = function.keysToIgnore()
  170. return differences.filter { !keysToIgnore.contains($0.key) }
  171. }
  172. private static func differencesArray(
  173. function: OrefFunction,
  174. swift: String,
  175. javascript: String
  176. ) throws -> [String: ValueDifference] {
  177. guard let jsData = javascript.data(using: .utf8),
  178. let swiftData = swift.data(using: .utf8),
  179. let jsArray = try JSONSerialization.jsonObject(with: jsData) as? [Any],
  180. let swiftArray = try JSONSerialization.jsonObject(with: swiftData) as? [Any]
  181. else {
  182. throw NSError(domain: "JSONBridge", code: 1, userInfo: [NSLocalizedDescriptionKey: "Invalid JSON format"])
  183. }
  184. // Converting arrays into dictionaries for comparison
  185. let jsDict = Dictionary(uniqueKeysWithValues: jsArray.enumerated().map { index, value in
  186. ("[\(index)]", value)
  187. })
  188. let swiftDict = Dictionary(uniqueKeysWithValues: swiftArray.enumerated().map { index, value in
  189. ("[\(index)]", value)
  190. })
  191. return compareDict(function: function, swiftDict: swiftDict, jsDict: jsDict)
  192. }
  193. private static func differencesDictionary(
  194. function: OrefFunction,
  195. swift: String,
  196. javascript: String
  197. ) throws -> [String: ValueDifference] {
  198. guard let jsData = javascript.data(using: .utf8),
  199. let swiftData = swift.data(using: .utf8),
  200. let jsDict = try JSONSerialization.jsonObject(with: jsData) as? [String: Any],
  201. let swiftDict = try JSONSerialization.jsonObject(with: swiftData) as? [String: Any]
  202. else {
  203. throw NSError(domain: "JSONBridge", code: 1, userInfo: [NSLocalizedDescriptionKey: "Invalid JSON format"])
  204. }
  205. return compareDict(function: function, swiftDict: swiftDict, jsDict: jsDict)
  206. }
  207. private static func compareDict(
  208. function: OrefFunction,
  209. swiftDict: [String: Any],
  210. jsDict: [String: Any]
  211. ) -> [String: ValueDifference] {
  212. var differences: [String: ValueDifference] = [:]
  213. let approximateKeys = function.approximateMatchingNumbers()
  214. // Check all keys present in either dictionary
  215. Set(jsDict.keys).union(swiftDict.keys).forEach { key in
  216. let jsValue = jsDict[key].map(convertToJSONValue) ?? .null
  217. let swiftValue = swiftDict[key].map(convertToJSONValue) ?? .null
  218. if !valuesAreEqual(jsValue, swiftValue, approximately: approximateKeys[key], approximateKeys: approximateKeys) {
  219. differences[key] = ValueDifference(
  220. js: jsValue,
  221. swift: swiftValue,
  222. jsKeyMissing: !jsDict.keys.contains(key),
  223. nativeKeyMissing: !swiftDict.keys.contains(key)
  224. )
  225. }
  226. }
  227. return differences
  228. }
  229. private static func convertToJSONValue(_ value: Any) -> JSONValue {
  230. switch value {
  231. case let string as String:
  232. return .string(string)
  233. case let number as NSNumber:
  234. // NSNumber can represent both booleans and numbers
  235. // Check if it's a boolean using the objCType
  236. let objCType = String(cString: number.objCType)
  237. if objCType == "c" || objCType == "B" { // These represent BOOLs in ObjC
  238. return .boolean(number.boolValue)
  239. } else {
  240. return .number(number.doubleValue)
  241. }
  242. case let bool as Bool:
  243. return .boolean(bool)
  244. case let array as [Any]:
  245. return .array(array.map(convertToJSONValue))
  246. case let dict as [String: Any]:
  247. return .object(dict.mapValues(convertToJSONValue))
  248. case is NSNull:
  249. return .null
  250. default:
  251. return .null
  252. }
  253. }
  254. private static func valuesAreEqual(
  255. _ value1: JSONValue,
  256. _ value2: JSONValue,
  257. approximately: Double?,
  258. approximateKeys: [String: Double]
  259. ) -> Bool {
  260. switch (value1, value2) {
  261. case (.null, .null):
  262. return true
  263. case let (.string(s1), .string(s2)):
  264. return s1 == s2
  265. case let (.number(n1), .number(n2)):
  266. let match = n1.isApproximatelyEqual(to: n2, epsilon: approximately)
  267. return match
  268. case let (.boolean(b1), .boolean(b2)):
  269. return b1 == b2
  270. case let (.array(a1), .array(a2)):
  271. return a1.count == a2.count && zip(a1, a2).allSatisfy { v1, v2 in
  272. valuesAreEqual(v1, v2, approximately: approximately, approximateKeys: approximateKeys)
  273. }
  274. case let (.object(o1), .object(o2)):
  275. return o1.keys == o2.keys && o1.keys.allSatisfy { key in
  276. guard let v1 = o1[key], let v2 = o2[key] else { return false }
  277. return valuesAreEqual(v1, v2, approximately: approximateKeys[key], approximateKeys: approximateKeys)
  278. }
  279. default:
  280. return false
  281. }
  282. }
  283. }