Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement reference coalescing when decoding documents with anchors i… #4

Open
wants to merge 4 commits into
base: anchor_tag_and_autoaliasing
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)

Expand Down
47 changes: 47 additions & 0 deletions Sources/Yams/AliasDereferencingStrategy.swift
Original file line number Diff line number Diff line change
@@ -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")!
}
1 change: 1 addition & 0 deletions Sources/Yams/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@

add_library(Yams
AliasDereferencingStrategy.swift
Anchor.swift
Constructor.swift
Decoder.swift
Expand Down
106 changes: 89 additions & 17 deletions Sources/Yams/Decoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -30,7 +53,12 @@ public class YAMLDecoder {
public func decode<T>(_ 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)
}
Expand All @@ -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
Expand Down Expand Up @@ -80,15 +108,18 @@ public class YAMLDecoder {
public func decode<T>(_ 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 {
Expand Down Expand Up @@ -295,29 +326,70 @@ extension _Decoder: SingleValueDecodingContainer {
// MARK: - Swift.SingleValueDecodingContainer Methods

func decodeNil() -> Bool { return node.null == NSNull() }
func decode<T>(_ type: T.Type) throws -> T where T: Decodable & ScalarConstructible { return try construct(type) }
func decode<T>(_ type: T.Type) throws -> T where T: Decodable {return try construct(type) ?? type.init(from: self) }
func decode<T>(_ type: T.Type) throws -> T where T: Decodable & ScalarConstructible { return try _decode(type) }
func decode<T>(_ type: T.Type) throws -> T where T: Decodable {return try _decode(type) }

// MARK: -

private func _decode<T: Decodable>(_ type: T.Type) throws -> T {
if let dereferenced = dereferenceAnchor(type) {
return dereferenced
}

let constructed = try _construct(type)

recordAnchor(constructed)

return constructed
}

private func _construct<T: Decodable>(_ 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<T: ScalarConstructible>(_ type: T.Type) throws -> T {
private func constructScalar<T: ScalarConstructible>(_ 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)
}
return constructed
}

private func construct<T>(_ type: T.Type) throws -> T? {
guard let constructibleType = type as? ScalarConstructible.Type else {
private func dereferenceAnchor<T>(_ 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<T: Decodable>(_ constructed: T) {
guard let anchor = self.node.anchor else {
return
}

guard let strategy = userInfo[.aliasDereferencingStrategy] as? any AliasDereferencingStrategy else {
return
}

return strategy[anchor] = constructed
}
}

Expand Down
2 changes: 1 addition & 1 deletion Tests/YamsTests/AnchorCodingTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
1 change: 1 addition & 0 deletions Tests/YamsTests/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
add_library(YamsTests
AnchorCodingTests.swift
AnchorTolerancesTests.swift
ClassReferenceDecodingTests.swift
ConstructorTests.swift
EmitterTests.swift
EncoderTests.swift
Expand Down
Loading
Loading