// // Node.swift // MongoView // // Created by Shadowfacts on 1/9/20. // Copyright © 2020 Shadowfacts. All rights reserved. // import Foundation import MongoSwift // Node needs to be an NSObject, since NSOutlineView uses NSObject.isEqual(_:) and NSObject.hash to determine item equality // which is necessary to prevent items from collapsing when refreshing the view class Node: NSObject { let key: Key? let value: BSON weak var parent: Node? lazy private(set) var children: [Node] = { switch value { case let .array(array): return array.enumerated().map { (index, val) in Node(key: .index(index), value: val, parent: self) } case let .document(doc): return doc.map { (key, val) in Node(key: .name(key), value: val, parent: self) } default: return [] } }() var numberOfChildren: Int { children.count } var hasChildren: Bool { numberOfChildren > 0 } init(key: Key? = nil, value: BSON, parent: Node? = nil) { self.value = value self.parent = parent if key == nil, case let .document(doc) = value, case let .objectID(id) = doc["_id"] { self.key = .objectID(id) } else { self.key = key } } convenience init(document: BSONDocument) { if case let .objectID(id) = document["_id"] { self.init(key: .objectID(id), value: .document(document)) } else { self.init(key: nil, value: .document(document)) } } override func isEqual(_ object: Any?) -> Bool { guard let object = object as? Node else { return false } return self.parent == object.parent && self.key == object.key } override var hash: Int { var hasher = Hasher() hasher.combine(parent) hasher.combine(key) return hasher.finalize() } } extension Node { enum Key: Equatable, Hashable { case index(Int) case name(String) case objectID(BSONObjectID) } } extension Node { static let dateFormatter: DateFormatter = { let formatter = DateFormatter() formatter.locale = .current formatter.setLocalizedDateFormatFromTemplate("yyyy-MM-dd HH:mm:ss ZZ") return formatter }() var keyString: String { switch key { case nil: return "" case let .index(index): return index.description case let .name(name): return name case let .objectID(id): return id.description } } var valueString: String { switch value { case let .double(value): return value.description case let .string(value): return value case let .document(doc): return "(\(doc.count) field\(doc.count == 1 ? "" : "s"))" case let .array(array): return "(\(array.count) element\(array.count == 1 ? "" : "s"))" case let .binary(value): switch value.subtype { case .generic: return "(generic binary data)" case .function: return "(function binary data)" case .binaryDeprecated: return "(binary data)" case .uuidDeprecated: fallthrough case .uuid: return try! value.toUUID().description case .md5: return "(MD5 binary data)" default: return "(unknown binary data))" } case .undefined: return "undefined" case let .objectID(value): return value.description case let .bool(value): return value.description case let .datetime(value): return value.description case .null: return "null" case let .regex(value): return value.pattern case let .dbPointer(value): return "\(value.ref)(\(value.id))" case let .symbol(value): return value.description case let .code(value): return value.code case let .codeWithScope(value): return value.code case let .int32(value): return value.description case let .timestamp(value): let date = Date(timeIntervalSince1970: TimeInterval(value.timestamp)) return Node.dateFormatter.string(from: date) case let .int64(value): return value.description case let .decimal128(value): return value.description case .minKey: return "(min key)" case .maxKey: return "(max key)" } } var typeString: String { switch value { case .double(_): return "Double" case .string(_): return "String" case .document(_): return "Document" case .array(_): return "Array" case let .binary(value): switch value.subtype { case .generic: return "Generic binary data" case .function: return "Function binary data" case .binaryDeprecated: return "Binary data" case .uuidDeprecated: fallthrough case .uuid: return "UUID" case .md5: return "MD5 hash" default: return "Unknown binary data" } case .undefined: return "Undefined" case .objectID(_): return "ObjectId" case .bool(_): return "Bool" case .datetime(_): return "DateTime" case .null: return "Null" case .regex(_): return "RegEx" case .dbPointer(_): return "DBRef" case .symbol(_): return "Symbol" case .code(_): return "Code" case .codeWithScope(_): return "Code with scope" case .int32(_): return "Int32" case .timestamp(_): return "Timestamp" case .int64(_): return "Int64" case .decimal128(_): return "Decimal128" case .minKey: return "MinKey" case .maxKey: return "MaxKey" } } var isValueCopyable: Bool { switch value { case .document(_), .array(_), .binary(_), .minKey, .maxKey: return false default: return true } } }