Last active
December 2, 2023 20:37
-
-
Save adam-zethraeus/da8ec7680aa96b06e2defc9655f6241f to your computer and use it in GitHub Desktop.
JSON.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
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