diff --git a/Sources/Yams/AliasDereferencingStrategy.swift b/Sources/Yams/AliasDereferencingStrategy.swift index 35547d5a..7a7a7736 100644 --- a/Sources/Yams/AliasDereferencingStrategy.swift +++ b/Sources/Yams/AliasDereferencingStrategy.swift @@ -9,15 +9,15 @@ import Foundation public protocol AliasDereferencingStrategy: AnyObject { - + subscript(_ key: Anchor) -> Any? { get set } } public class BasicAliasDereferencingStrategy: AliasDereferencingStrategy { public init() {} - + private var map: [Anchor: Any] = .init() - + public subscript(_ key: Anchor) -> Any? { get { map[key] } set { map[key] = newValue } diff --git a/Sources/Yams/CMakeLists.txt b/Sources/Yams/CMakeLists.txt index 0bababc9..ac25b581 100644 --- a/Sources/Yams/CMakeLists.txt +++ b/Sources/Yams/CMakeLists.txt @@ -1,5 +1,6 @@ add_library(Yams + AliasDereferencingStrategy.swift Anchor.swift Constructor.swift Decoder.swift diff --git a/Sources/Yams/Decoder.swift b/Sources/Yams/Decoder.swift index 3f34ab2f..b3428d38 100644 --- a/Sources/Yams/Decoder.swift +++ b/Sources/Yams/Decoder.swift @@ -18,17 +18,17 @@ public class YAMLDecoder { self.encoding = encoding self.aliasDereferencingStrategy = aliasDereferencingStrategy } - + /// Encoding public var encoding: Parser.Encoding = .default - + /// Alias dereferencing strategy to use when decoding. Defaults to nil public var aliasDereferencingStrategy: AliasDereferencingStrategy? } - + /// Options to use when decoding from YAML. public var options = Options() - + /// Creates a `YAMLDecoder` instance. /// /// - parameter encoding: String encoding, @@ -36,7 +36,7 @@ public class YAMLDecoder { self.init() self.options.encoding = encoding } - + /// Creates a `YAMLDecoder` instance. public init() {} @@ -56,7 +56,7 @@ public class YAMLDecoder { if let dealiasingStrategy = options.aliasDereferencingStrategy { finalUserInfo[.aliasDereferencingStrategy] = dealiasingStrategy } - + let decoder = _Decoder(referencing: node, userInfo: finalUserInfo) let container = try decoder.singleValueContainer() return try container.decode(type) @@ -334,26 +334,26 @@ extension _Decoder: SingleValueDecodingContainer { if let dereferenced = dereferenceAnchor(type) { return dereferenced } - + let constructed = try _construct(type) - + recordAnchor(constructed) - + return constructed } - + private func _construct(_ type: T.Type) throws -> T { if let constructibleType = type as? ScalarConstructible.Type { let scalarConstructed = try constructScalar(constructibleType) - guard let t = scalarConstructed as? T else { + guard let scalarT = scalarConstructed as? T else { throw _typeMismatch(at: codingPath, expectation: type, reality: scalarConstructed) } - return t + return scalarT } // not scalar constructable, initialize as Decodable return try type.init(from: self) } - + /// constuct `T` from `node` private func constructScalar(_ type: T.Type) throws -> T { let scalar = try self.scalar() @@ -362,33 +362,32 @@ extension _Decoder: SingleValueDecodingContainer { } return constructed } - - + private func dereferenceAnchor(_ type: T.Type) -> T? { guard let anchor = self.node.anchor else { return nil } - + guard let strategy = userInfo[.aliasDereferencingStrategy] as? any AliasDereferencingStrategy else { return nil } - + guard let existing = strategy[anchor] as? T else { return nil } - + return existing } - + private func recordAnchor(_ constructed: T) { guard let anchor = self.node.anchor else { return } - + guard let strategy = userInfo[.aliasDereferencingStrategy] as? any AliasDereferencingStrategy else { return } - + return strategy[anchor] = constructed } } diff --git a/Tests/YamsTests/CMakeLists.txt b/Tests/YamsTests/CMakeLists.txt index 8b223cac..932d019c 100644 --- a/Tests/YamsTests/CMakeLists.txt +++ b/Tests/YamsTests/CMakeLists.txt @@ -1,6 +1,7 @@ add_library(YamsTests AnchorCodingTests.swift AnchorTolerancesTests.swift + ClassReferenceDecodingTests.swift ConstructorTests.swift EmitterTests.swift EncoderTests.swift diff --git a/Tests/YamsTests/ClassReferenceDecodingTests.swift b/Tests/YamsTests/ClassReferenceDecodingTests.swift index 64069375..64cac6c5 100644 --- a/Tests/YamsTests/ClassReferenceDecodingTests.swift +++ b/Tests/YamsTests/ClassReferenceDecodingTests.swift @@ -30,17 +30,17 @@ class ClassReferenceDecodingTests: XCTestCase { - *simple """ ) - + guard let decoded else { return } - + XCTAssertTrue(decoded[0] === decoded[1], "Class reference not unique") } - + /// If types conform to YamlAnchorProviding and are Hashable-Equal then HashableAliasingStrategy aliases them func testEncoderAutoAlias_Hashable_duplicateAnchor_objectCoalescing() throws { let simpleStruct1 = SimpleWithAnchor(nested: .init(stringValue: "it's a value"), intValue: 52) let simpleStruct2 = SimpleWithAnchor(nested: .init(stringValue: "it's a value"), intValue: 52) - + let sameTypeOneAnchorPair = SimplePair(first: simpleStruct1, second: simpleStruct2) let encodingOptions = YAMLEncoder.Options(redundancyAliasingStrategy: HashableAliasingStrategy()) @@ -57,9 +57,9 @@ class ClassReferenceDecodingTests: XCTestCase { second: *simple """ ) - + guard let decoded else { return } - + XCTAssertTrue(decoded.first === decoded.first, "Class reference not unique") } @@ -82,9 +82,9 @@ class ClassReferenceDecodingTests: XCTestCase { - *2 """ ) - + guard let decoded else { return } - + XCTAssertTrue(decoded[0] === decoded[1], "Class reference not unique") } @@ -115,9 +115,9 @@ class ClassReferenceDecodingTests: XCTestCase { intValue: *4 """ ) - + guard let decoded else { return } - + XCTAssertTrue(decoded.first.nested === decoded.second.nested, "Class reference not unique") } @@ -150,9 +150,9 @@ class ClassReferenceDecodingTests: XCTestCase { intValue: *5 """ ) - + guard let decoded else { return } - + XCTAssertTrue(decoded.first.nested === decoded.second.nested, "Class reference not unique") } @@ -181,21 +181,20 @@ class ClassReferenceDecodingTests: XCTestCase { second: *2 """ ) - + guard let decoded else { return } - + /// It is expected and rational behavior that if an aliased value is decoded into two different types /// that those types cannot share object identity (a memory address) XCTAssertTrue(decoded.first !== decoded.second, "Class reference is unique") - - + /// It would be nice, - /// if objects contained within aliased values which are decoded different types could still identify and preserve the - /// object identity of those contained objects. (If ivars of different types could share reference to common data) - /// but is asking too much.... + /// if objects contained within aliased values which are decoded different types could still identify and + /// preserve the object identity of those contained objects. + /// (If ivars of different types could share reference to common data) + /// but is asking too much.... XCTAssertFalse(decoded.first.nested === decoded.second.nested, "You fixed it!") - - + /// The reality of the behavior is that if you declared to decode an aliased value into two different classes, /// you forfeit the possibility of down-graph reference sharing. XCTAssertTrue(decoded.first.nested !== decoded.second.nested, "Class reference is unique") @@ -213,15 +212,15 @@ class ClassReferenceDecodingTests: XCTestCase { private class NestedStruct: Codable, Hashable { let stringValue: String - + init(stringValue: String) { self.stringValue = stringValue } - + static func == (lhs: NestedStruct, rhs: NestedStruct) -> Bool { lhs.stringValue == rhs.stringValue } - + func hash(into hasher: inout Hasher) { hasher.combine(stringValue) } @@ -238,19 +237,19 @@ private class SimpleWithAnchor: SimpleProtocol, YamlAnchorProviding { let nested: NestedStruct let intValue: Int let yamlAnchor: Anchor? - + init(nested: NestedStruct, intValue: Int, yamlAnchor: Anchor? = "simple") { self.nested = nested self.intValue = intValue self.yamlAnchor = yamlAnchor } - + static func == (lhs: SimpleWithAnchor, rhs: SimpleWithAnchor) -> Bool { lhs.nested == rhs.nested && lhs.intValue == rhs.intValue && lhs.yamlAnchor == rhs.yamlAnchor } - + func hash(into hasher: inout Hasher) { hasher.combine(nested) hasher.combine(intValue) @@ -261,17 +260,17 @@ private class SimpleWithAnchor: SimpleProtocol, YamlAnchorProviding { private class SimpleWithoutAnchor: SimpleProtocol { let nested: NestedStruct let intValue: Int - + init(nested: NestedStruct, intValue: Int) { self.nested = nested self.intValue = intValue } - + static func == (lhs: SimpleWithoutAnchor, rhs: SimpleWithoutAnchor) -> Bool { lhs.nested == rhs.nested && lhs.intValue == rhs.intValue } - + func hash(into hasher: inout Hasher) { hasher.combine(nested) hasher.combine(intValue) @@ -279,51 +278,50 @@ private class SimpleWithoutAnchor: SimpleProtocol { } private class SimpleWithoutAnchor2: SimpleProtocol { - + let nested: NestedStruct let intValue: Int // swiftlint:disable unused_declaration let unrelatedValue: String? - + init(nested: NestedStruct, intValue: Int, unrelatedValue: String? = nil) { self.nested = nested self.intValue = intValue self.unrelatedValue = unrelatedValue } - static func == (lhs: SimpleWithoutAnchor2, rhs: SimpleWithoutAnchor2) -> Bool { lhs.nested == rhs.nested && lhs.intValue == rhs.intValue && lhs.unrelatedValue == rhs.unrelatedValue } - + func hash(into hasher: inout Hasher) { hasher.combine(nested) hasher.combine(intValue) hasher.combine(unrelatedValue) } - + } private class SimpleWithStringTypeAnchorName: SimpleProtocol { - + let nested: NestedStruct let intValue: Int let yamlAnchor: String? - + init(nested: NestedStruct, intValue: Int, yamlAnchor: String? = "StringTypeAnchor") { self.nested = nested self.intValue = intValue self.yamlAnchor = yamlAnchor } - + static func == (lhs: SimpleWithStringTypeAnchorName, rhs: SimpleWithStringTypeAnchorName) -> Bool { lhs.nested == rhs.nested && lhs.intValue == rhs.intValue && lhs.yamlAnchor == rhs.yamlAnchor } - + func hash(into hasher: inout Hasher) { hasher.combine(nested) hasher.combine(intValue) diff --git a/Tests/YamsTests/EncoderTests.swift b/Tests/YamsTests/EncoderTests.swift index 4d85fb2d..0b7dd39b 100644 --- a/Tests/YamsTests/EncoderTests.swift +++ b/Tests/YamsTests/EncoderTests.swift @@ -463,7 +463,7 @@ where T: Codable, T: Equatable { let decoded = try decoder.decode(T.self, from: producedYAML) XCTAssertEqual(decoded, value, "\(T.self) did not round-trip to an equal value.", file: (file), line: line) - + return decoded } catch let error as EncodingError { @@ -473,7 +473,7 @@ where T: Codable, T: Equatable { } catch { XCTFail("Rout trip test of \(T.self) failed with error: \(error)", file: (file), line: line) } - + return nil }