diff --git a/README.md b/README.md index 7c67fa3..fe9b304 100644 --- a/README.md +++ b/README.md @@ -138,6 +138,31 @@ try await req.client.post(uri, headers: headers) { inoutReq in } ``` +*setNull & Compact Map Values* +```swift +let json = """ +{ + "name": "Lookup", + "age": 3, + "versions": [ + "2.4.0", "2.0.0", "1.0.0" + ], + "birth": "202" +} +let lookup = Lookup(json) +print(lookup.versions.0.string) // -> "2.4.0" +lookup["versions.0"] = "2.4.1" +print(lookup.versions.0.string) // -> "2.4.1" + +// setNull +let newLookup = lookup.setNull(keys: ["birth"]) +newLookup.hasKey("birth") // -> true, but value is null(nil) + +let compactLookup = newLookup.compactMapValues() +newLookup.hasKey("birth") // -> false, no key no value +""" +``` + More usage references `LookupTests.swift`: [LookupTests.swift](https://github.com/iWECon/Lookup/blob/main/Tests/LookupTests/LookupTests.swift) diff --git a/Sources/Lookup/Lookup.swift b/Sources/Lookup/Lookup.swift index f6907a5..b171f55 100644 --- a/Sources/Lookup/Lookup.swift +++ b/Sources/Lookup/Lookup.swift @@ -46,7 +46,7 @@ fileprivate func unwrap(_ object: Any?) -> Any { // MARK: - Lookup @dynamicMemberLookup -public struct Lookup: Swift.CustomStringConvertible, Swift.CustomDebugStringConvertible, @unchecked Sendable { +public struct Lookup: @unchecked Sendable { public enum RawType { case none @@ -76,11 +76,11 @@ public struct Lookup: Swift.CustomStringConvertible, Swift.CustomDebugStringConv } } - fileprivate let rawType: RawType - fileprivate private(set) var rawDict: [String: Any] = [:] - fileprivate private(set) var rawArray: [Any] = [] - fileprivate private(set) var rawString: String = "" - fileprivate private(set) var rawNumber: NSNumber = 0 + var rawType: RawType + var rawDict: [String: Any] = [:] + var rawArray: [Any] = [] + var rawString: String = "" + var rawNumber: NSNumber = 0 private init(jsonObject: Any) { switch jsonObject { @@ -201,18 +201,61 @@ public struct Lookup: Swift.CustomStringConvertible, Swift.CustomDebugStringConv } } - private mutating func setNewValue(for dynamicMember: String, value: Lookup) { + /** + + let json = { + "name": "Lookup", + "age": 18, + "versions: [ + "1.0.0", "1.1.0", "1.2.0", "1.3.0" + ] + } + + let lookup = Lookup(json) + // > print(lookup.versions.0) -> output: 1.0.0 + lookup["versions.0"] = "0.0.1" + // > print(lookup.versions.0) -> output: 0.0.1 + // > print(lookup.versions.1) -> output: 1.0.0 + */ + private mutating func setNewValue(for dynamicMember: String, value: Lookup, inner: Bool = false) { + if dynamicMember.contains(".") { + var keys = dynamicMember.components(separatedBy: ".") + let finalKey = keys.removeLast() + let newKeys = keys.joined(separator: ".") + var _value = self[dynamicMember: newKeys] + + switch _value.rawType { + case .none, .number, .string: + _value = value + case .dict, .object: + _value.rawDict[finalKey] = value.rawValue + case .array: + if inner { + _value.rawArray = [value.rawValue] + } else { + if finalKey.isPurnInt, let index = Int(finalKey) { + _value.rawArray.insert(value.rawValue, at: index) + } else { + _value.rawArray = [value.rawValue] + } + } + } + + setNewValue(for: newKeys, value: _value, inner: true) + return + } + switch rawType { - case .none: - break + case .none, .number, .string: + self = value case .dict, .object: rawDict[dynamicMember] = value.rawValue case .array: - rawArray = value.rawArray - case .number: - rawNumber = value.rawNumber - case .string: - rawString = value.rawString + if dynamicMember.isPurnInt, let index = Int(dynamicMember) { + rawArray.insert(value.rawValue, at: index) + } else { + rawArray = value.rawArray + } } } @@ -251,34 +294,6 @@ public struct Lookup: Swift.CustomStringConvertible, Swift.CustomDebugStringConv } } - private func castValueToString(value: Any) -> String { - if let data: Data = try? JSONSerialization.data(withJSONObject: value, options: .prettyPrinted), - let str = String(data: data, encoding: .utf8) - { - return str - } - return "Can not cast value to string" - } - - public var description: String { - let desc: String - switch rawType { - case .dict, .object: - desc = castValueToString(value: rawDict) - case .array: - desc = castValueToString(value: rawArray) - case .number: - desc = "\(rawNumber)" - case .string: - desc = "\(rawValue)" - case .none: - desc = "nil" - } - return desc - } - - public var debugDescription: String { description } - fileprivate static var null: Lookup { Lookup(NSNull()) } fileprivate mutating func merge(other: Lookup) { @@ -826,3 +841,87 @@ extension Lookup: Codable { } } } + +// MARK: Description +extension Lookup: Swift.CustomStringConvertible, Swift.CustomDebugStringConvertible { + + private func castValueToString(value: Any) -> String { + if #available(macOS 10.15, *) { + if let data: Data = try? JSONSerialization.data(withJSONObject: value, options: [.sortedKeys, .prettyPrinted, .fragmentsAllowed, .withoutEscapingSlashes]), + let str = String(data: data, encoding: .utf8) + { + return str + } + } else { + // Fallback on earlier versions + if let data: Data = try? JSONSerialization.data(withJSONObject: value, options: [.sortedKeys, .prettyPrinted, .fragmentsAllowed]), + let str = String(data: data, encoding: .utf8) + { + return str + } + } + return "Can not cast value to string" + } + + public var description: String { + let desc: String + switch rawType { + case .dict, .object: + desc = castValueToString(value: rawDict) + case .array: + desc = castValueToString(value: rawArray) + case .number: + desc = "\(rawNumber)" + case .string: + desc = "\(rawValue)" + case .none: + desc = "nil" + } + return desc + } + + public var debugDescription: String { description } + +} + +// MARK: - Filter +extension Lookup { + + public func keep(keys: [String]) -> Lookup { + let res = keys.map { key -> (String, Any?) in + (key, self[key].rawValue) + } + return Lookup(Dictionary(uniqueKeysWithValues: res)) + } + + public func setNull(keys: [String]) -> Lookup { + var new = self + for key in keys { + new[dynamicMember: key] = nil + } + return new + } + + public func compactMapValues(keepEmptyValue: Bool = false) -> Lookup { + switch rawType { + case .none, .number: + return self + case .string: + return self + case .array: + let map = rawArray.map { value in + Lookup(value).compactMapValues() + } + return Lookup(map) + case .object, .dict: + let map = rawDict.compactMap { (k: String, v: Any) -> (String, Any)? in + let vl = Lookup(v) + if vl.isNone || (keepEmptyValue && vl.isEmpty) { + return nil + } + return (k, vl.compactMapValues()) + } + return Lookup(Dictionary(uniqueKeysWithValues: map)) + } + } +} diff --git a/Tests/LookupTests/LookupTests.swift b/Tests/LookupTests/LookupTests.swift index 6d38659..e142a92 100644 --- a/Tests/LookupTests/LookupTests.swift +++ b/Tests/LookupTests/LookupTests.swift @@ -238,11 +238,10 @@ final class LookupTests: XCTestCase { lookup["address"] = "in Hangzhou" XCTAssertEqual(lookup.address.string, "in Hangzhou") - - // TODO: dynamicMember change + lookup["list.0"] = "d" - XCTAssertNotEqual(lookup.list.0.string, "d") - + XCTAssertEqual(lookup.list.0.string, "d") + let jsonData = try JSONEncoder().encode(lookup) let _jsonString = String(data: jsonData, encoding: .utf8) XCTAssertNotNil(_jsonString) @@ -325,6 +324,73 @@ final class LookupTests: XCTestCase { } try changeValue() + func testSelect() throws { + + struct User { + let id: Int + let name: String + let age: Int + } + let user = User(id: 1, name: "wei", age: 18) + let lookup = Lookup(user) + let keepLookup = lookup.keep(keys: ["name"]) + + XCTAssertEqual(keepLookup.id.isNone, true) + XCTAssertEqual(keepLookup.name.string, "wei") + /** + { + id: xxx, + cars: [Car] + } + let lookup = Lookup(cars) + + let carsLookup = Lookup(cars) + carsLookup.0.name = .null + carsLookup.compactMapValues() + + lookup.cars = carsLookup + */ + let rejectLookup = lookup.setNull(keys: ["id"]) + .compactMapValues() + XCTAssertEqual(rejectLookup.id.isNone, true) + XCTAssertEqual(rejectLookup.name.string, "wei") + XCTAssertEqual(rejectLookup.age.int, 18) + + var aLookup = Lookup( + ["name": "iwecon", "childs":[ + [ + "name": "lookup", "id": nil, "age": 18, + "childs": [ + ["name": "Lookup.dynamicMember", "age": 12, "id": nil] + ] + ] + ]] + ) + .compactMapValues() + XCTAssertEqual(aLookup.hasKey("childs.0.id"), false) + XCTAssertEqual(aLookup.hasKey("childs.0.age"), true) + XCTAssertEqual(aLookup.hasKey("childs.0.childs.0.id"), false) + XCTAssertEqual(aLookup.childs.0.age.int, 18) + XCTAssertEqual(aLookup.childs.0.childs.0.name.string, "Lookup.dynamicMember") + + aLookup["childs.0.id"] = "1" + XCTAssertEqual(aLookup.childs.0.id.string, "1") + + let anlookup = aLookup.setNull(keys: ["childs.0.age"]) + XCTAssertEqual(anlookup.hasKey("childs.0.age"), true) + var canlookup = anlookup.compactMapValues() + XCTAssertEqual(canlookup.hasKey("childs.0.age"), false) + + canlookup["childs.0.childs.0.id"] = 1 + canlookup["childs.0.childs.0.age"] = 18 + XCTAssertEqual(canlookup.childs.0.childs.0.id.int, 1) + XCTAssertEqual(canlookup.childs.0.childs.0.age.int, 18) + + canlookup["childs.0.childs.0.birth"] = "2024" + XCTAssertEqual(canlookup.childs.0.childs.0.birth.string, "2024") + } + try testSelect() + #if os(iOS) func uiView() throws { let view = UIView()