diff --git a/CHANGELOG.md b/CHANGELOG.md index e477d0a7..bc4fdd70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,21 @@ ##### Enhancements +* Yams is able to coalesce references to objects decoded with YAML anchors. + [Adora Lynch](https://github.com/lynchsft) + +##### Bug Fixes + +* None. + +## 5.3.0 + +##### Breaking + +* None. + +##### Enhancements + * Yams is able to encode and decode Anchors via YamlAnchorProviding, and YamlAnchorCoding. [Adora Lynch](https://github.com/lynchsft) @@ -16,7 +31,7 @@ [Adora Lynch](https://github.com/lynchsft) [#265](https://github.com/jpsim/Yams/issues/265) -* Yams is able to detect redundant structes and automaticaly +* Yams is able to detect redundant structs and automatically alias them during encoding via RedundancyAliasingStrategy [Adora Lynch](https://github.com/lynchsft) diff --git a/Sources/Yams/AliasDereferencingStrategy.swift b/Sources/Yams/AliasDereferencingStrategy.swift new file mode 100644 index 00000000..d8701d32 --- /dev/null +++ b/Sources/Yams/AliasDereferencingStrategy.swift @@ -0,0 +1,47 @@ +// +// AliasDereferencingStrategy.swift +// Yams +// +// Created by Adora Lynch on 8/9/24. +// Copyright (c) 2024 Yams. All rights reserved. +// + +/// A class-bound protocol which implements a strategy for dereferencing aliases (or dealiasing) values during +/// YAML document decoding. YAML documents which do not contain anchors will not benefit from the use of +/// an AliasDereferencingStrategy in any way. The main use-case for dereferencing aliases in a YML document +/// is when decoding into class types. If the yaml document is large and contains many references +/// (perhaps it is a representation of a dense graph) then, decoding into structs will require the of large amounts +/// of system memory to represent highly redundant (duplicated) data structures. +/// However, if the same document is decoded into class types and the decoding uses +/// an `AliasDereferencingStrategy` such as `BasicAliasDereferencingStrategy` then the emitted value will have its +/// class references coalesced. No duplicate objects will be initialized (unless identical objects have multiple +/// distinct anchors in the YAML document). In some scenarios this may significantly reduce the memory footprint of +/// the decoded type. +public protocol AliasDereferencingStrategy: AnyObject { + /// The stored exestential type of all AliasDereferencingStrategys + typealias Value = (any Decodable) + /// get and set cached references, keyed bo an Anchor + subscript(_ key: Anchor) -> Value? { get set } +} + +/// A AliasDereferencingStrategy which caches all values (even value-type values) in a Dictionary, +/// keyed by their Anchor. +/// For reference types, this strategy achieves reference coalescing +/// For value types, this strategy achieves short-cutting the decoding process when dereferencing aliases. +/// if the aliased structure is large, this may result in a time savings +public class BasicAliasDereferencingStrategy: AliasDereferencingStrategy { + /// Create a new BasicAliasDereferencingStrategy + public init() {} + + private var map: [Anchor: Value] = .init() + + /// get and set cached references, keyed bo an Anchor + public subscript(_ key: Anchor) -> Value? { + get { map[key] } + set { map[key] = newValue } + } +} + +extension CodingUserInfoKey { + internal static let aliasDereferencingStrategy = Self(rawValue: "aliasDereferencingStrategy")! +} 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 859c4583..270f4412 100644 --- a/Sources/Yams/Decoder.swift +++ b/Sources/Yams/Decoder.swift @@ -11,13 +11,36 @@ import Foundation /// `Codable`-style `Decoder` that can be used to decode a `Decodable` type from a given `String` and optional /// user info mapping. Similar to `Foundation.JSONDecoder`. public class YAMLDecoder { + /// Options to use when decoding from YAML. + public struct Options { + /// Create `YAMLDecoder.Options` with the specified values. + public init(encoding: Parser.Encoding = .default, + aliasDereferencingStrategy: AliasDereferencingStrategy? = nil) { + 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: Encoding, `.default` if omitted. - public init(encoding: Parser.Encoding = .default) { - self.encoding = encoding + /// - parameter encoding: String encoding, + public convenience init(encoding: Parser.Encoding) { + self.init() + self.options.encoding = encoding } + /// Creates a `YAMLDecoder` instance. + public init() {} + /// Decode a `Decodable` type from a given `Node` and optional user info mapping. /// /// - parameter type: `Decodable` type to decode. @@ -30,7 +53,12 @@ public class YAMLDecoder { public func decode(_ type: T.Type = T.self, from node: Node, userInfo: [CodingUserInfoKey: Any] = [:]) throws -> T where T: Swift.Decodable { - let decoder = _Decoder(referencing: node, userInfo: userInfo) + var finalUserInfo = userInfo + 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) } @@ -48,7 +76,7 @@ public class YAMLDecoder { from yaml: String, userInfo: [CodingUserInfoKey: Any] = [:]) throws -> T where T: Swift.Decodable { do { - let parser = try Parser(yaml: yaml, resolver: Resolver([.merge]), encoding: encoding) + let parser = try Parser(yaml: yaml, resolver: Resolver([.merge]), encoding: options.encoding) // ^ the parser holds the references to Anchors while parsing, return try withExtendedLifetime(parser) { // ^ so we hold an explicit reference to the parser during decoding @@ -80,15 +108,18 @@ public class YAMLDecoder { public func decode(_ type: T.Type = T.self, from yamlData: Data, userInfo: [CodingUserInfoKey: Any] = [:]) throws -> T where T: Swift.Decodable { - guard let yamlString = String(data: yamlData, encoding: encoding.swiftStringEncoding) else { - throw YamlError.dataCouldNotBeDecoded(encoding: encoding.swiftStringEncoding) + guard let yamlString = String(data: yamlData, encoding: options.encoding.swiftStringEncoding) else { + throw YamlError.dataCouldNotBeDecoded(encoding: options.encoding.swiftStringEncoding) } return try decode(type, from: yamlString, userInfo: userInfo) } /// Encoding - public var encoding: Parser.Encoding + @available(*, deprecated, renamed: "options.encoding") + public var encoding: Parser.Encoding { + options.encoding + } } private struct _Decoder: Decoder { @@ -295,13 +326,37 @@ extension _Decoder: SingleValueDecodingContainer { // MARK: - Swift.SingleValueDecodingContainer Methods func decodeNil() -> Bool { return node.null == NSNull() } - func decode(_ type: T.Type) throws -> T where T: Decodable & ScalarConstructible { return try construct(type) } - func decode(_ type: T.Type) throws -> T where T: Decodable {return try construct(type) ?? type.init(from: self) } + func decode(_ type: T.Type) throws -> T where T: Decodable & ScalarConstructible { return try _decode(type) } + func decode(_ type: T.Type) throws -> T where T: Decodable {return try _decode(type) } // MARK: - + private func _decode(_ type: T.Type) throws -> T { + 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 scalarT = scalarConstructed as? T else { + throw _typeMismatch(at: codingPath, expectation: type, reality: scalarConstructed) + } + return scalarT + } + // not scalar constructable, initialize as Decodable + return try type.init(from: self) + } + /// constuct `T` from `node` - private func construct(_ type: T.Type) throws -> T { + private func constructScalar(_ type: T.Type) throws -> T { let scalar = try self.scalar() guard let constructed = type.construct(from: scalar) else { throw _typeMismatch(at: codingPath, expectation: type, reality: scalar) @@ -309,15 +364,32 @@ extension _Decoder: SingleValueDecodingContainer { return constructed } - private func construct(_ type: T.Type) throws -> T? { - guard let constructibleType = type as? ScalarConstructible.Type else { + private func dereferenceAnchor(_ type: T.Type) -> T? { + guard let anchor = self.node.anchor else { return nil } - let scalar = try self.scalar() - guard let value = constructibleType.construct(from: scalar) else { - throw _valueNotFound(at: codingPath, type, "Expected \(type) value but found \(scalar) instead.") + + guard let strategy = userInfo[.aliasDereferencingStrategy] as? any AliasDereferencingStrategy else { + return nil } - return value as? T + + 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/AnchorCodingTests.swift b/Tests/YamsTests/AnchorCodingTests.swift index 997ced71..a14ef575 100644 --- a/Tests/YamsTests/AnchorCodingTests.swift +++ b/Tests/YamsTests/AnchorCodingTests.swift @@ -79,7 +79,7 @@ class AnchorCodingTests: XCTestCase { stringValue: it's a value intValue: 52 - """.data(using: decoder.encoding.swiftStringEncoding)! + """.data(using: decoder.options.encoding.swiftStringEncoding)! let decodedStruct = try decoder.decode(SimpleWithStringTypeAnchorName.self, from: data) 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 new file mode 100644 index 00000000..2d84f6b1 --- /dev/null +++ b/Tests/YamsTests/ClassReferenceDecodingTests.swift @@ -0,0 +1,329 @@ +// +// ClassReferenceDecodingTests.swift +// Yams +// +// Created by Adora Lynch on 8/9/24. +// Copyright (c) 2024 Yams. All rights reserved. +// + +import XCTest +import Yams + +class ClassReferenceDecodingTests: XCTestCase { + + /// If types conform to YamlAnchorProviding and are Hashable-Equal then HashableAliasingStrategy aliases them + func testEncoderAutoAlias_Hashable_duplicateAnchor() throws { + let simpleStruct = SimpleWithAnchor(nested: .init(stringValue: "it's a value"), intValue: 52) + let duplicatedStructArray = [simpleStruct, simpleStruct] + + let encodingOptions = YAMLEncoder.Options(redundancyAliasingStrategy: HashableAliasingStrategy()) + let decodingOptions = YAMLDecoder.Options(aliasDereferencingStrategy: BasicAliasDereferencingStrategy()) + let decoded = + _testRoundTrip(of: duplicatedStructArray, + with: encodingOptions, + decodingOptions: decodingOptions, + expectedYAML: """ + - &simple + nested: + stringValue: it's a value + intValue: 52 + - *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()) + let decodingOptions = YAMLDecoder.Options(aliasDereferencingStrategy: BasicAliasDereferencingStrategy()) + let decoded = + _testRoundTrip(of: sameTypeOneAnchorPair, + with: encodingOptions, + decodingOptions: decodingOptions, + expectedYAML: """ + first: &simple + nested: + stringValue: it's a value + intValue: 52 + second: *simple + + """ ) + + guard let decoded else { return } + + XCTAssertTrue(decoded.first === decoded.first, "Class reference not unique") + } + + /// If types do NOT conform to YamlAnchorProviding and are Hashable-Equal then HashableAliasingStrategy aliases them + func testEncoderAutoAlias_Hashable_noAnchors() throws { + let simpleStruct = SimpleWithoutAnchor(nested: .init(stringValue: "it's a value"), intValue: 52) + let duplicatedStructArray = [simpleStruct, simpleStruct] // zero specified anchor + + let encodingOptions = YAMLEncoder.Options(redundancyAliasingStrategy: HashableAliasingStrategy()) + let decodingOptions = YAMLDecoder.Options(aliasDereferencingStrategy: BasicAliasDereferencingStrategy()) + let decoded = + _testRoundTrip(of: duplicatedStructArray, + with: encodingOptions, + decodingOptions: decodingOptions, + expectedYAML: """ + - &2 + nested: + stringValue: it's a value + intValue: 52 + - *2 + + """ ) + + guard let decoded else { return } + + XCTAssertTrue(decoded[0] === decoded[1], "Class reference not unique") + } + + /// If types conform to YamlAnchorProviding and are NOT Hashable-Equal then + /// HashableAliasingStrategy does not alias them even though their members may still be + /// Hashable-Equal and therefor maybe aliased. + func testEncoderAutoAlias_Hashable_uniqueAnchor() throws { + let differentTypesOneAnchors = SimplePair(first: + SimpleWithAnchor(nested: .init(stringValue: "it's a value"), + intValue: 52), + second: + SimpleWithoutAnchor(nested: .init(stringValue: "it's a value"), + intValue: 52)) + + let encodingOptions = YAMLEncoder.Options(redundancyAliasingStrategy: HashableAliasingStrategy()) + let decodingOptions = YAMLDecoder.Options(aliasDereferencingStrategy: BasicAliasDereferencingStrategy()) + let decoded = + _testRoundTrip(of: differentTypesOneAnchors, + with: encodingOptions, + decodingOptions: decodingOptions, + expectedYAML: """ + first: &simple + nested: &2 + stringValue: it's a value + intValue: &4 52 + second: + nested: *2 + intValue: *4 + + """ ) + + guard let decoded else { return } + + XCTAssertTrue(decoded.first.nested === decoded.second.nested, "Class reference not unique") + } + + /// If types conform to YamlAnchorProviding and are NOT Hashable-Equal then + /// HashableAliasingStrategy does not alias them even though their members may still be + /// Hashable-Equal and therefor maybe aliased. + /// Note particularly that the to Simple* values here have exactly the same encoded representation, + /// they're just different types and thus not Hashable-Equal + func testEncoderAutoAlias_Hashable_noAnchor() throws { + let differentTypesNoAnchors = SimplePair(first: + SimpleWithoutAnchor2(nested: .init(stringValue: "it's a value"), + intValue: 52), + second: + SimpleWithoutAnchor(nested: .init(stringValue: "it's a value"), + intValue: 52)) + + let encodingOptions = YAMLEncoder.Options(redundancyAliasingStrategy: HashableAliasingStrategy()) + let decodingOptions = YAMLDecoder.Options(aliasDereferencingStrategy: BasicAliasDereferencingStrategy()) + let decoded = + _testRoundTrip(of: differentTypesNoAnchors, + with: encodingOptions, + decodingOptions: decodingOptions, + expectedYAML: """ + first: + nested: &3 + stringValue: it's a value + intValue: &5 52 + second: + nested: *3 + intValue: *5 + + """ ) + + guard let decoded else { return } + + XCTAssertTrue(decoded.first.nested === decoded.second.nested, "Class reference not unique") + } + + /// If types conform to YamlAnchorProviding and have exactly the same encoded representation then + /// StrictEncodableAliasingStrategy alias them even though they are encoded and decoded from + /// different types. + func testEncoderAutoAlias_StrictEncodable_NoAnchors() throws { + let differentTypesNoAnchors = SimplePair(first: + SimpleWithoutAnchor2(nested: .init(stringValue: "it's a value"), + intValue: 52), + second: + SimpleWithoutAnchor(nested: .init(stringValue: "it's a value"), + intValue: 52)) + + let encodingOptions = YAMLEncoder.Options(redundancyAliasingStrategy: StrictEncodableAliasingStrategy()) + let decodingOptions = YAMLDecoder.Options(aliasDereferencingStrategy: BasicAliasDereferencingStrategy()) + let decoded = + _testRoundTrip(of: differentTypesNoAnchors, + with: encodingOptions, + decodingOptions: decodingOptions, + expectedYAML: """ + first: &2 + nested: + stringValue: it's a value + intValue: 52 + 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.... + 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") + } + + /// A type used to contain values used during testing + private struct SimplePair: Hashable, Codable { + let first: First + let second: Second + } + +} + +// MARK: - Types used for Anchor encoding tests. + +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) + } +} +private protocol SimpleProtocol: Codable, Hashable { + // swiftlint:disable unused_declaration + var nested: NestedStruct { get } + // swiftlint:disable unused_declaration + var intValue: Int { get } +} + +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) + hasher.combine(yamlAnchor) + } +} + +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) + } +} + +private class SimpleWithoutAnchor2: SimpleProtocol { + + let nested: NestedStruct + let intValue: Int + 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) + hasher.combine(yamlAnchor) + } +} diff --git a/Tests/YamsTests/EncoderTests.swift b/Tests/YamsTests/EncoderTests.swift index 0d85a3a2..0b7dd39b 100644 --- a/Tests/YamsTests/EncoderTests.swift +++ b/Tests/YamsTests/EncoderTests.swift @@ -439,11 +439,14 @@ class EncoderTests: XCTestCase { // swiftlint:disable:this type_body_length } } +/// returns the decoded instance of T +@discardableResult internal func _testRoundTrip(of value: T, with options: YAMLEncoder.Options = .init(), + decodingOptions: YAMLDecoder.Options = .init(), expectedYAML yamlString: String? = nil, file: StaticString = #file, - line: UInt = #line) + line: UInt = #line) -> T? where T: Codable, T: Equatable { do { let encoder = YAMLEncoder() @@ -456,10 +459,13 @@ where T: Codable, T: Equatable { } let decoder = YAMLDecoder() + decoder.options = decodingOptions 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 { XCTFail("Failed to encode \(T.self) from YAML by error: \(error)", file: (file), line: line) } catch let error as DecodingError { @@ -467,6 +473,8 @@ where T: Codable, T: Equatable { } catch { XCTFail("Rout trip test of \(T.self) failed with error: \(error)", file: (file), line: line) } + + return nil } // MARK: - Helper Global Functions diff --git a/Tests/YamsTests/TagCodingTests.swift b/Tests/YamsTests/TagCodingTests.swift index 6f85828d..4a6ba967 100644 --- a/Tests/YamsTests/TagCodingTests.swift +++ b/Tests/YamsTests/TagCodingTests.swift @@ -77,7 +77,7 @@ class TagCodingTests: XCTestCase { stringValue: it's a value intValue: 52 - """.data(using: decoder.encoding.swiftStringEncoding)! + """.data(using: decoder.options.encoding.swiftStringEncoding)! let decodedStruct = try decoder.decode(SimpleWithStringTypeTagName.self, from: data) diff --git a/Yams.xcodeproj/project.pbxproj b/Yams.xcodeproj/project.pbxproj index f6cc6ca1..cb66bde8 100644 --- a/Yams.xcodeproj/project.pbxproj +++ b/Yams.xcodeproj/project.pbxproj @@ -44,6 +44,8 @@ 8FBD7F8B2CB70CFB00271BB9 /* AnchorTolerancesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FBD7F852CB70CFB00271BB9 /* AnchorTolerancesTests.swift */; }; 8FBD7F8C2CB70CFB00271BB9 /* TagTolerancesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FBD7F882CB70CFB00271BB9 /* TagTolerancesTests.swift */; }; 8FBD7F8D2CB70CFB00271BB9 /* TagCodingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FBD7F872CB70CFB00271BB9 /* TagCodingTests.swift */; }; + 8FCB2E202CF6D82700550869 /* ClassReferenceDecodingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FCB2E1F2CF6D82700550869 /* ClassReferenceDecodingTests.swift */; }; + 8FCB2E222CF6D83F00550869 /* AliasDereferencingStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FCB2E212CF6D83F00550869 /* AliasDereferencingStrategy.swift */; }; E8EDB8851DE2181B0062268D /* api.c in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_10 /* api.c */; }; E8EDB8871DE2181B0062268D /* emitter.c in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_12 /* emitter.c */; }; E8EDB8891DE2181B0062268D /* parser.c in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_14 /* parser.c */; }; @@ -107,6 +109,8 @@ 8FBD7F862CB70CFB00271BB9 /* NodeDecoderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeDecoderTests.swift; sourceTree = ""; }; 8FBD7F872CB70CFB00271BB9 /* TagCodingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagCodingTests.swift; sourceTree = ""; }; 8FBD7F882CB70CFB00271BB9 /* TagTolerancesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagTolerancesTests.swift; sourceTree = ""; }; + 8FCB2E1F2CF6D82700550869 /* ClassReferenceDecodingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClassReferenceDecodingTests.swift; sourceTree = ""; }; + 8FCB2E212CF6D83F00550869 /* AliasDereferencingStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AliasDereferencingStrategy.swift; sourceTree = ""; }; OBJ_10 /* api.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = api.c; sourceTree = ""; }; OBJ_12 /* emitter.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = emitter.c; sourceTree = ""; }; OBJ_14 /* parser.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = parser.c; sourceTree = ""; }; @@ -151,6 +155,7 @@ OBJ_21 /* Yams */ = { isa = PBXGroup; children = ( + 8FCB2E212CF6D83F00550869 /* AliasDereferencingStrategy.swift */, 8FBD7F7A2CB70C8900271BB9 /* Anchor.swift */, 6C0D2A351E0A934B00C45545 /* Constructor.swift */, 6C4AF31E1EBE14A1008775BC /* Decoder.swift */, @@ -191,6 +196,7 @@ children = ( 8FBD7F842CB70CFB00271BB9 /* AnchorCodingTests.swift */, 8FBD7F852CB70CFB00271BB9 /* AnchorTolerancesTests.swift */, + 8FCB2E1F2CF6D82700550869 /* ClassReferenceDecodingTests.swift */, 6C0488ED1E0CBD56006F9F80 /* ConstructorTests.swift */, 6C0A00D41E152D6200222704 /* EmitterTests.swift */, 6C788A001EB87232005386F0 /* EncoderTests.swift */, @@ -385,6 +391,7 @@ 6C0409AA1E6033DF00C95D83 /* Node.Sequence.swift in Sources */, 6C0409A81E602E9A00C95D83 /* Node.Mapping.swift in Sources */, E8EDB88C1DE2181B0062268D /* writer.c in Sources */, + 8FCB2E222CF6D83F00550869 /* AliasDereferencingStrategy.swift in Sources */, E8EDB88B1DE2181B0062268D /* scanner.c in Sources */, 6C4AF3201EBE1705008775BC /* Decoder.swift in Sources */, 6C0409AC1E607E9900C95D83 /* Node.Scalar.swift in Sources */, @@ -414,6 +421,7 @@ 8FBD7F892CB70CFB00271BB9 /* NodeDecoderTests.swift in Sources */, 8FBD7F8A2CB70CFB00271BB9 /* AnchorCodingTests.swift in Sources */, 8FBD7F8B2CB70CFB00271BB9 /* AnchorTolerancesTests.swift in Sources */, + 8FCB2E202CF6D82700550869 /* ClassReferenceDecodingTests.swift in Sources */, 8FBD7F8C2CB70CFB00271BB9 /* TagTolerancesTests.swift in Sources */, 8FBD7F8D2CB70CFB00271BB9 /* TagCodingTests.swift in Sources */, 6C78C5651E29B27D0096215F /* RepresenterTests.swift in Sources */,