Skip to content

Instantly share code, notes, and snippets.

@adam-zethraeus
Last active December 2, 2023 20:37
Show Gist options
  • Save adam-zethraeus/da8ec7680aa96b06e2defc9655f6241f to your computer and use it in GitHub Desktop.
Save adam-zethraeus/da8ec7680aa96b06e2defc9655f6241f to your computer and use it in GitHub Desktop.
JSON.swift
/*
This source file is part of the Swift.org open source project
Copyright (c) 2021 Apple Inc. and the Swift project authors
Licensed under Apache License v2.0 with Runtime Library Exception
See https://swift.org/LICENSE.txt for license information
See https://swift.org/CONTRIBUTORS.txt for Swift project authors
*/
// Source: https://github.com/apple/swift-docc/blob/ddf8f0a64f4f477259ae166b84d5f064ab2887c5/bin/output-diff.swift
import Foundation
/// A struct to decode any kind of JSON element.
indirect enum JSON: Codable {
case dictionary([String: JSON])
case array([JSON])
case string(String)
case number(Double)
case boolean(Bool)
case null
struct Key: CodingKey {
init(_ value: String) {
self.value = value
}
init?(intValue: Int) {
value = "\(intValue)"
}
init?(stringValue: String) {
value = stringValue
}
let value: String
public var stringValue: String {
value
}
public var intValue: Int? {
Int(value)
}
}
init(from decoder: any Decoder) throws {
let container = try decoder.singleValueContainer()
if container.decodeNil() {
self = .null
} else if let boolValue = try? container.decode(Bool.self) {
self = .boolean(boolValue)
} else if let numericValue = try? container.decode(Double.self) {
self = .number(numericValue)
} else if let stringValue = try? container.decode(String.self) {
self = .string(stringValue)
} else if let arrayValue = try? container.decode([JSON].self) {
self = .array(arrayValue)
} else {
self = .dictionary(try container.decode([String: JSON].self))
}
}
func encode(to encoder: any Encoder) throws {
switch self {
case .dictionary(let dictionary):
var container = encoder.container(keyedBy: Key.self)
for (k, v) in dictionary {
try container.encode(v, forKey: Key(k))
}
case .array(let array):
var container = encoder.unkeyedContainer()
for i in array {
try container.encode(i)
}
case .string(let string):
var container = encoder.singleValueContainer()
try container.encode(string)
case .number(let double):
var container = encoder.singleValueContainer()
try container.encode(double)
case .boolean(let bool):
var container = encoder.singleValueContainer()
try container.encode(bool)
case .null:
var container = encoder.singleValueContainer()
try container.encodeNil()
}
}
/// Compares two `JSON` values recursively and produces detailed summary.
public static func compare(lhs: JSON, rhs: JSON, diff: inout [String], path: [String] = [""]) -> Bool {
switch (lhs, rhs) {
case let (.array(lhs), .array(rhs)):
guard lhs.count == rhs.count else {
diff.append("Error at path \(path.joined(separator: "/")): \(lhs.count) != \(rhs.count) array elements")
return false
}
// Ignore the order of elements in arrays where it doesn't matter
if flagIgnoreArrayOrderForPaths.first(where: { path.joined(separator: "/").contains($0) }) != nil {
var result = true
for (offset, value) in lhs.enumerated() {
var valueDiff = [String]()
let valueFound = rhs.contains(where: {
return JSON.compare(lhs: value, rhs: $0, diff: &valueDiff, path: [])
})
guard valueFound else {
let keyPath = path + ["\(offset)"]
diff.append("Error at path \(keyPath.joined(separator: "/")): Element not found in after version of the array.")
result = false
continue
}
}
return result
} else {
return lhs.enumerated().reduce(into: true) { (result, pair) in
var valueDiff = [String]()
let valueResult = JSON.compare(lhs: lhs[pair.offset], rhs: rhs[pair.offset], diff: &valueDiff, path: path + [String(pair.offset)])
diff.append(contentsOf: valueDiff)
result = result && valueResult
}
}
case let (.dictionary(lhs), .dictionary(rhs)):
guard lhs.keys == rhs.keys else {
for change in Array(lhs.keys.sorted()).difference(from: rhs.keys.sorted()) {
switch change {
case .insert(_, let element, _): diff.append("Error at path \(path.joined(separator: "/")): Removed key '\(element)'")
case .remove(_, let element, _): diff.append("Error at path \(path.joined(separator: "/")): Added key '\(element)'")
}
}
return false
}
return lhs.keys.reduce(into: true) { (result, key) in
var keyDiff = [String]()
let keyResult = JSON.compare(lhs: lhs[key]!, rhs: rhs[key]!, diff: &keyDiff, path: path + [key])
diff.append(contentsOf: keyDiff)
result = result && keyResult
}
case let (.string(lhs), .string(rhs)):
guard lhs == rhs else {
diff.append("Error at path \(path.joined(separator: "/")): '\(lhs)' != '\(rhs)'")
return false
}
return true
case let (.number(lhs), .number(rhs)):
guard lhs == rhs else {
diff.append("Error at path \(path.joined(separator: "/")): '\(lhs)' != '\(rhs)'")
return false
}
return true
case let (.boolean(lhs), .boolean(rhs)):
guard lhs == rhs else {
diff.append("Error at path \(path.joined(separator: "/")): '\(lhs)' != '\(rhs)'")
return false
}
return true
case (.null, .null): return true
default: return false
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment