From aa983d880093fc54816a4bd8f10a10dcff1f6e28 Mon Sep 17 00:00:00 2001 From: Adora Lynch Date: Thu, 19 Sep 2024 22:47:33 -0400 Subject: [PATCH 1/7] Implement Anchor coding, Tag coding and redundancy auto-aliasing --- CHANGELOG.md | 26 ++ Sources/Yams/Anchor.swift | 36 +++ Sources/Yams/Constructor.swift | 2 + Sources/Yams/Decoder.swift | 51 ++- Sources/Yams/Emitter.swift | 124 +++++--- Sources/Yams/Encoder.swift | 65 +++- Sources/Yams/Node.Alias.swift | 58 ++++ Sources/Yams/Node.Mapping.swift | 7 +- Sources/Yams/Node.Scalar.swift | 5 +- Sources/Yams/Node.Sequence.swift | 5 +- Sources/Yams/Node.swift | 64 +++- Sources/Yams/Parser.swift | 55 ++-- Sources/Yams/RedundancyAliasingStrategy.swift | 117 +++++++ Sources/Yams/Resolver.swift | 2 + Sources/Yams/Tag.swift | 20 ++ Sources/Yams/YamlAnchorProviding.swift | 45 +++ Sources/Yams/YamlTagProviding.swift | 43 +++ Tests/YamsTests/AnchorCodingTests.swift | 298 ++++++++++++++++++ Tests/YamsTests/AnchorTolerancesTests.swift | 162 ++++++++++ Tests/YamsTests/EncoderTests.swift | 58 ++-- Tests/YamsTests/TagCodingTests.swift | 240 ++++++++++++++ Tests/YamsTests/TagTolerancesTests.swift | 179 +++++++++++ 22 files changed, 1541 insertions(+), 121 deletions(-) create mode 100644 Sources/Yams/Anchor.swift create mode 100644 Sources/Yams/Node.Alias.swift create mode 100644 Sources/Yams/RedundancyAliasingStrategy.swift create mode 100644 Sources/Yams/YamlAnchorProviding.swift create mode 100644 Sources/Yams/YamlTagProviding.swift create mode 100644 Tests/YamsTests/AnchorCodingTests.swift create mode 100644 Tests/YamsTests/AnchorTolerancesTests.swift create mode 100644 Tests/YamsTests/TagCodingTests.swift create mode 100644 Tests/YamsTests/TagTolerancesTests.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 82045a5d..e477d0a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,32 @@ ##### Breaking +* None. + +##### Enhancements + +* Yams is able to encode and decode Anchors via YamlAnchorProviding, and + YamlAnchorCoding. + [Adora Lynch](https://github.com/lynchsft) + [#125](https://github.com/jpsim/Yams/issues/125) + +* Yams is able to encode and decode Tags via YamlTagProviding + and YamlTagCoding. + [Adora Lynch](https://github.com/lynchsft) + [#265](https://github.com/jpsim/Yams/issues/265) + +* Yams is able to detect redundant structes and automaticaly + alias them during encoding via RedundancyAliasingStrategy + [Adora Lynch](https://github.com/lynchsft) + +##### Bug Fixes + +* None. + +## 5.2.0 + +##### Breaking + * Swift 5.7 or later is now required to build Yams. [JP Simard](https://github.com/jpsim) diff --git a/Sources/Yams/Anchor.swift b/Sources/Yams/Anchor.swift new file mode 100644 index 00000000..6e63d256 --- /dev/null +++ b/Sources/Yams/Anchor.swift @@ -0,0 +1,36 @@ +// +// Anchor.swift +// Yams +// +// Created by Adora Lynch on 8/9/24. +// Copyright (c) 2024 Yams. All rights reserved. + +import Foundation + +public final class Anchor: RawRepresentable, ExpressibleByStringLiteral, Codable, Hashable { + + public static let permittedCharacters = CharacterSet.lowercaseLetters + .union(.uppercaseLetters) + .union(.decimalDigits) + .union(.init(charactersIn: "-_")) + + public static func is_cyamlAlpha(_ string: String) -> Bool { + Anchor.permittedCharacters.isSuperset(of: .init(charactersIn: string)) + } + + + public let rawValue: String + + public init(rawValue: String) { + self.rawValue = rawValue + } + + public init(stringLiteral value: String) { + rawValue = value + } +} + +extension Anchor: CustomStringConvertible { + public var description: String { rawValue } +} + diff --git a/Sources/Yams/Constructor.swift b/Sources/Yams/Constructor.swift index 4120202c..e93ed2c5 100644 --- a/Sources/Yams/Constructor.swift +++ b/Sources/Yams/Constructor.swift @@ -54,6 +54,8 @@ public final class Constructor { return result } return [Any].construct_seq(from: sequence) + case .alias(_): + preconditionFailure("Aliases should be resolved before construction") } } diff --git a/Sources/Yams/Decoder.swift b/Sources/Yams/Decoder.swift index ae0b7dba..ffd294b8 100644 --- a/Sources/Yams/Decoder.swift +++ b/Sources/Yams/Decoder.swift @@ -48,8 +48,17 @@ public class YAMLDecoder { from yaml: String, userInfo: [CodingUserInfoKey: Any] = [:]) throws -> T where T: Swift.Decodable { do { - let node = try Parser(yaml: yaml, resolver: Resolver([.merge]), encoding: encoding).singleRoot() ?? "" - return try self.decode(type, from: node, userInfo: userInfo) + let parser = try Parser(yaml: yaml, resolver: Resolver([.merge]), encoding: 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 + let node = try parser.singleRoot() ?? "" + // ^ nodes only have weak references to Anchors (the Anchors would disappear if not held by the parser) + return try self.decode(type, from: node, userInfo: userInfo) + // ^ if the decoded type or contained types are YamlAnchorCoding, + // those types have taken ownership of Anchors. + // Otherwise the Anchors are deallocated when this function exits just like Tag and Mark + } } catch let error as DecodingError { throw error } catch { @@ -129,6 +138,8 @@ private struct _Decoder: Decoder { throw _typeMismatch(at: codingPath, expectation: Node.Scalar.self, reality: mapping) case .sequence(let sequence): throw _typeMismatch(at: codingPath, expectation: Node.Scalar.self, reality: sequence) + case .alias(let alias): + throw _typeMismatch(at: codingPath, expectation: Node.Scalar.self, reality: alias) } } } @@ -140,7 +151,41 @@ private struct _KeyedDecodingContainer: KeyedDecodingContainerPr init(decoder: _Decoder, wrapping mapping: Node.Mapping) { self.decoder = decoder - self.mapping = mapping + + let keys = mapping.keys + + let decodeAnchor: Anchor? + let decodeTag: Tag? + + if let anchor = mapping.anchor, keys.contains(.anchorKeyNode) == false { + decodeAnchor = anchor + } else { + decodeAnchor = nil + } + + if mapping.tag.name != .implicit && keys.contains(.tagKeyNode) == false { + decodeTag = mapping.tag + } else { + decodeTag = nil + } + + switch (decodeAnchor, decodeTag) { + case (nil, nil): + self.mapping = mapping + case (let anchor?, nil): + var mutableMapping = mapping + mutableMapping[.anchorKeyNode] = .scalar(.init(anchor.rawValue)) + self.mapping = mutableMapping + case (nil, let tag?): + var mutableMapping = mapping + mutableMapping[.tagKeyNode] = .scalar(.init(tag.name.rawValue)) + self.mapping = mutableMapping + case let (anchor?, tag?): + var mutableMapping = mapping + mutableMapping[.anchorKeyNode] = .scalar(.init(anchor.rawValue)) + mutableMapping[.tagKeyNode] = .scalar(.init(tag.name.rawValue)) + self.mapping = mutableMapping + } } // MARK: - Swift.KeyedDecodingContainerProtocol Methods diff --git a/Sources/Yams/Emitter.swift b/Sources/Yams/Emitter.swift index 6eeee9aa..565e1162 100644 --- a/Sources/Yams/Emitter.swift +++ b/Sources/Yams/Emitter.swift @@ -99,7 +99,8 @@ public func dump( sortKeys: Bool = false, sequenceStyle: Node.Sequence.Style = .any, mappingStyle: Node.Mapping.Style = .any, - newLineScalarStyle: Node.Scalar.Style = .any) throws -> String { + newLineScalarStyle: Node.Scalar.Style = .any, + redundancyAliasingStrategy: RedundancyAliasingStrategy? = nil) throws -> String { return try serialize( node: object.represented(), canonical: canonical, @@ -113,7 +114,8 @@ public func dump( sortKeys: sortKeys, sequenceStyle: sequenceStyle, mappingStyle: mappingStyle, - newLineScalarStyle: newLineScalarStyle + newLineScalarStyle: newLineScalarStyle, + redundancyAliasingStrategy: redundancyAliasingStrategy ) } @@ -148,7 +150,8 @@ public func serialize( sortKeys: Bool = false, sequenceStyle: Node.Sequence.Style = .any, mappingStyle: Node.Mapping.Style = .any, - newLineScalarStyle: Node.Scalar.Style = .any) throws -> String + newLineScalarStyle: Node.Scalar.Style = .any, + redundancyAliasingStrategy: RedundancyAliasingStrategy? = nil) throws -> String where Nodes: Sequence, Nodes.Iterator.Element == Node { let emitter = Emitter( canonical: canonical, @@ -162,7 +165,8 @@ public func serialize( sortKeys: sortKeys, sequenceStyle: sequenceStyle, mappingStyle: mappingStyle, - newLineScalarStyle: newLineScalarStyle + newLineScalarStyle: newLineScalarStyle, + redundancyAliasingStrategy: redundancyAliasingStrategy ) try emitter.open() try nodes.forEach(emitter.serialize) @@ -201,7 +205,8 @@ public func serialize( sortKeys: Bool = false, sequenceStyle: Node.Sequence.Style = .any, mappingStyle: Node.Mapping.Style = .any, - newLineScalarStyle: Node.Scalar.Style = .any) throws -> String { + newLineScalarStyle: Node.Scalar.Style = .any, + redundancyAliasingStrategy: RedundancyAliasingStrategy? = nil) throws -> String { return try serialize( nodes: [node], canonical: canonical, @@ -215,7 +220,8 @@ public func serialize( sortKeys: sortKeys, sequenceStyle: sequenceStyle, mappingStyle: mappingStyle, - newLineScalarStyle: newLineScalarStyle + newLineScalarStyle: newLineScalarStyle, + redundancyAliasingStrategy: redundancyAliasingStrategy ) } @@ -246,10 +252,10 @@ public final class Emitter { public var allowUnicode: Bool = false /// Set the preferred line break. public var lineBreak: LineBreak = .ln - - // internal since we don't know if these should be exposed. - var explicitStart: Bool = false - var explicitEnd: Bool = false + /// Set to emit an explicit document start marker. + public var explicitStart: Bool = false + /// Set to emit an explicit document end marker. + public var explicitEnd: Bool = false /// The `%YAML` directive value or nil. public var version: (major: Int, minor: Int)? @@ -265,6 +271,49 @@ public final class Emitter { /// Set the style for scalars that include newlines public var newLineScalarStyle: Node.Scalar.Style = .any + + /// Redundancy aliasing strategy to use when encoding. Defaults to nil + public var redundancyAliasingStrategy: RedundancyAliasingStrategy? + + /// Create `Emitter.Options` with the specified values. + /// + /// - parameter canonical: Set if the output should be in the "canonical" format described in the YAML + /// specification. + /// - parameter indent: Set the indentation value. + /// - parameter width: Set the preferred line width. -1 means unlimited. + /// - parameter allowUnicode: Set if unescaped non-ASCII characters are allowed. + /// - parameter lineBreak: Set the preferred line break. + /// - parameter explicitStart: Explicit document start `---`. + /// - parameter explicitEnd: Explicit document end `...`. + /// - parameter version: The `%YAML` directive value or nil. + /// - parameter sortKeys: Set if emitter should sort keys in lexicographic order. + /// - parameter sequenceStyle: Set the style for sequences (arrays / lists) + /// - parameter mappingStyle: Set the style for mappings (dictionaries) + /// - parameter newLineScalarStyle: Set the style for newline-containing scalars + /// - parameter redundancyAliasingStrategy: Set the strategy for identifying redundant structures and automatically aliasing them + public init(canonical: Bool = false, indent: Int = 0, width: Int = 0, allowUnicode: Bool = false, + lineBreak: Emitter.LineBreak = .ln, + explicitStart: Bool = false, + explicitEnd: Bool = false, + version: (major: Int, minor: Int)? = nil, + sortKeys: Bool = false, sequenceStyle: Node.Sequence.Style = .any, + mappingStyle: Node.Mapping.Style = .any, + newLineScalarStyle: Node.Scalar.Style = .any, + redundancyAliasingStrategy: RedundancyAliasingStrategy? = nil) { + self.canonical = canonical + self.indent = indent + self.width = width + self.allowUnicode = allowUnicode + self.lineBreak = lineBreak + self.explicitStart = explicitStart + self.explicitEnd = explicitEnd + self.version = version + self.sortKeys = sortKeys + self.sequenceStyle = sequenceStyle + self.mappingStyle = mappingStyle + self.newLineScalarStyle = newLineScalarStyle + self.redundancyAliasingStrategy = redundancyAliasingStrategy + } } /// Configuration options to use when emitting YAML. @@ -288,6 +337,8 @@ public final class Emitter { /// - parameter sortKeys: Set if emitter should sort keys in lexicographic order. /// - parameter sequenceStyle: Set the style for sequences (arrays / lists) /// - parameter mappingStyle: Set the style for mappings (dictionaries) + /// - parameter newLineScalarStyle: Set the style for newline-containing scalars + /// - parameter redundancyAliasingStrategy: Set the strategy for identifying redundant structures and automatically aliasing them public init(canonical: Bool = false, indent: Int = 0, width: Int = 0, @@ -299,7 +350,8 @@ public final class Emitter { sortKeys: Bool = false, sequenceStyle: Node.Sequence.Style = .any, mappingStyle: Node.Mapping.Style = .any, - newLineScalarStyle: Node.Scalar.Style = .any) { + newLineScalarStyle: Node.Scalar.Style = .any, + redundancyAliasingStrategy: RedundancyAliasingStrategy? = nil) { options = Options(canonical: canonical, indent: indent, width: width, @@ -311,7 +363,8 @@ public final class Emitter { sortKeys: sortKeys, sequenceStyle: sequenceStyle, mappingStyle: mappingStyle, - newLineScalarStyle: newLineScalarStyle) + newLineScalarStyle: newLineScalarStyle, + redundancyAliasingStrategy: redundancyAliasingStrategy) // configure emitter yaml_emitter_initialize(&emitter) yaml_emitter_set_output(&self.emitter, { pointer, buffer, size in @@ -413,38 +466,10 @@ public final class Emitter { } } -// MARK: - Options Initializer +//// MARK: - Options Initializer extension Emitter.Options { - /// Create `Emitter.Options` with the specified values. - /// - /// - parameter canonical: Set if the output should be in the "canonical" format described in the YAML - /// specification. - /// - parameter indent: Set the indentation value. - /// - parameter width: Set the preferred line width. -1 means unlimited. - /// - parameter allowUnicode: Set if unescaped non-ASCII characters are allowed. - /// - parameter lineBreak: Set the preferred line break. - /// - parameter explicitStart: Explicit document start `---`. - /// - parameter explicitEnd: Explicit document end `...`. - /// - parameter version: The `%YAML` directive value or nil. - /// - parameter sortKeys: Set if emitter should sort keys in lexicographic order. - /// - parameter sequenceStyle: Set the style for sequences (arrays / lists) - /// - parameter mappingStyle: Set the style for mappings (dictionaries) - public init(canonical: Bool = false, indent: Int = 0, width: Int = 0, allowUnicode: Bool = false, - lineBreak: Emitter.LineBreak = .ln, version: (major: Int, minor: Int)? = nil, - sortKeys: Bool = false, sequenceStyle: Node.Sequence.Style = .any, - mappingStyle: Node.Mapping.Style = .any, newLineScalarStyle: Node.Scalar.Style = .any) { - self.canonical = canonical - self.indent = indent - self.width = width - self.allowUnicode = allowUnicode - self.lineBreak = lineBreak - self.version = version - self.sortKeys = sortKeys - self.sequenceStyle = sequenceStyle - self.mappingStyle = mappingStyle - self.newLineScalarStyle = newLineScalarStyle - } + } // MARK: Implementation Details @@ -461,8 +486,17 @@ extension Emitter { case .scalar(let scalar): try serializeScalar(scalar) case .sequence(let sequence): try serializeSequence(sequence) case .mapping(let mapping): try serializeMapping(mapping) + case .alias(let alias): try serializeAlias(alias) } } + + private func serializeAlias(_ alias: Node.Alias) throws { + var event = yaml_event_t() + let anchor = alias.anchor.rawValue + yaml_alias_event_initialize(&event, + anchor) + try emit(&event) + } private func serializeScalar(_ scalar: Node.Scalar) throws { var value = scalar.string.utf8CString, tag = scalar.resolvedTag.name.rawValue.utf8CString @@ -472,7 +506,7 @@ extension Emitter { tag.withUnsafeMutableBytes { tag in yaml_scalar_event_initialize( &event, - nil, + scalar.anchor?.rawValue, tag.baseAddress?.assumingMemoryBound(to: UInt8.self), value.baseAddress?.assumingMemoryBound(to: UInt8.self), Int32(value.count - 1), @@ -492,7 +526,7 @@ extension Emitter { _ = tag.withUnsafeMutableBytes { tag in yaml_sequence_start_event_initialize( &event, - nil, + sequence.anchor?.rawValue, tag.baseAddress?.assumingMemoryBound(to: UInt8.self), implicit, sequenceStyle) @@ -511,7 +545,7 @@ extension Emitter { _ = tag.withUnsafeMutableBytes { tag in yaml_mapping_start_event_initialize( &event, - nil, + mapping.anchor?.rawValue, tag.baseAddress?.assumingMemoryBound(to: UInt8.self), implicit, mappingStyle) diff --git a/Sources/Yams/Encoder.swift b/Sources/Yams/Encoder.swift index fd17bac3..39b58ac3 100644 --- a/Sources/Yams/Encoder.swift +++ b/Sources/Yams/Encoder.swift @@ -28,10 +28,17 @@ public class YAMLEncoder { /// - throws: `EncodingError` if something went wrong while encoding. public func encode(_ value: T, userInfo: [CodingUserInfoKey: Any] = [:]) throws -> String { do { - let encoder = _Encoder(userInfo: userInfo, sequenceStyle: options.sequenceStyle, - mappingStyle: options.mappingStyle, newlineScalarStyle: options.newLineScalarStyle) + var finalUserInfo = userInfo + if let aliasingStrategy = options.redundancyAliasingStrategy { + finalUserInfo[.redundancyAliasingStrategyKey] = aliasingStrategy + } + let encoder = _Encoder(userInfo: finalUserInfo, + sequenceStyle: options.sequenceStyle, + mappingStyle: options.mappingStyle, + newlineScalarStyle: options.newLineScalarStyle) var container = encoder.singleValueContainer() try container.encode(value) + try options.redundancyAliasingStrategy?.releaseAnchorReferences() return try serialize(node: encoder.node, options: options) } catch let error as EncodingError { throw error @@ -165,7 +172,15 @@ private struct _KeyedEncodingContainer: KeyedEncodingContainerPr var codingPath: [CodingKey] { return encoder.codingPath } func encodeNil(forKey key: Key) throws { encoder.mapping[key.stringValue] = .null } func encode(_ value: T, forKey key: Key) throws where T: YAMLEncodable { try encoder(for: key).encode(value) } - func encode(_ value: T, forKey key: Key) throws where T: Encodable { try encoder(for: key).encode(value) } + func encode(_ value: T, forKey key: Key) throws where T: Encodable { + if let anchor = value as? Anchor, key.stringValue == Node.anchorKeyNode.string { + encoder.node = encoder.node.setting(anchor: anchor) + } else if let tag = value as? Tag, key.stringValue == Node.tagKeyNode.string { + encoder.node = encoder.node.setting(tag: tag) + } else { + try encoder(for: key).encode(value) + } + } func nestedContainer(keyedBy type: NestedKey.Type, forKey key: Key) -> KeyedEncodingContainer { @@ -225,21 +240,49 @@ extension _Encoder: SingleValueEncodingContainer { func encode(_ value: T) throws where T: YAMLEncodable { assertCanEncodeNewValue() - node = value.box() - if let stringValue = value as? (any StringProtocol), stringValue.contains("\n") { - node.scalar?.style = newlineScalarStyle + try encode(yamlEncodable: value) + } + + private func encode(yamlEncodable encodable: YAMLEncodable) throws { + func encodeNode() { + node = encodable.box() + if let stringValue = encodable as? (any StringProtocol), stringValue.contains("\n") { + node.scalar?.style = newlineScalarStyle + } + } + if let redundancyAliasingStrategy = userInfo[.redundancyAliasingStrategyKey] as? RedundancyAliasingStrategy { + switch try redundancyAliasingStrategy.alias(for: encodable) { + case .none: + encodeNode() + case let .anchor(anchor): + encodeNode() + self.node = self.node.setting(anchor: anchor) + case let .alias(anchor): + self.node = .alias(.init(anchor)) + } + } else { + encodeNode() } } func encode(_ value: T) throws where T: Encodable { assertCanEncodeNewValue() if let encodable = value as? YAMLEncodable { - node = encodable.box() - if let stringValue = value as? (any StringProtocol), stringValue.contains("\n") { - node.scalar?.style = newlineScalarStyle - } + try encode(yamlEncodable: encodable) } else { - try value.encode(to: self) + if let redundancyAliasingStrategy = userInfo[.redundancyAliasingStrategyKey] as? RedundancyAliasingStrategy { + switch try redundancyAliasingStrategy.alias(for: value) { + case .none: + try value.encode(to: self) + case let .anchor(anchor): + try value.encode(to: self) + self.node = self.node.setting(anchor: anchor) + case let .alias(anchor): + self.node = .alias(.init(anchor)) + } + } else { + try value.encode(to: self) + } } } diff --git a/Sources/Yams/Node.Alias.swift b/Sources/Yams/Node.Alias.swift new file mode 100644 index 00000000..1c3585bb --- /dev/null +++ b/Sources/Yams/Node.Alias.swift @@ -0,0 +1,58 @@ +// +// Node.Alias.swift +// Yams +// +// Created by Adora Lynch on 8/19/24. +// Copyright (c) 2024 Yams. All rights reserved. +// + +import Foundation + +// MARK: Node+Alias + +extension Node { + /// Scalar node. + public struct Alias { + /// The anchor for this alias. + public var anchor: Anchor + /// This node's tag (its type). + public var tag: Tag + /// The location for this node. + public var mark: Mark? + + /// Create a `Node.Alias` using the specified parameters. + /// + /// - parameter tag: This scalar's `Tag`. + /// - parameter mark: This scalar's `Mark`. + public init(_ anchor: Anchor, _ tag: Tag = .implicit, _ mark: Mark? = nil) { + self.anchor = anchor + self.tag = tag + self.mark = mark + } + } +} + +extension Node.Alias: Comparable { + /// :nodoc: + public static func < (lhs: Node.Alias, rhs: Node.Alias) -> Bool { + lhs.anchor.rawValue < rhs.anchor.rawValue + } +} + +extension Node.Alias: Equatable { + /// :nodoc: + public static func == (lhs: Node.Alias, rhs: Node.Alias) -> Bool { + lhs.anchor == rhs.anchor + } +} + +extension Node.Alias: Hashable { + /// :nodoc: + public func hash(into hasher: inout Hasher) { + hasher.combine(anchor) + } +} + +extension Node.Alias: TagResolvable { + static let defaultTagName = Tag.Name.implicit +} diff --git a/Sources/Yams/Node.Mapping.swift b/Sources/Yams/Node.Mapping.swift index a6feafc0..e9635fb8 100644 --- a/Sources/Yams/Node.Mapping.swift +++ b/Sources/Yams/Node.Mapping.swift @@ -16,6 +16,8 @@ extension Node { public var style: Style /// This mapping's `Mark`. public var mark: Mark? + /// The anchor for this node. + public weak var anchor: Anchor? /// The style to use when emitting a `Mapping`. public enum Style: UInt32 { @@ -33,11 +35,12 @@ extension Node { /// - parameter tag: This mapping's `Tag`. /// - parameter style: The style to use when emitting this `Mapping`. /// - parameter mark: This mapping's `Mark`. - public init(_ pairs: [(Node, Node)], _ tag: Tag = .implicit, _ style: Style = .any, _ mark: Mark? = nil) { + public init(_ pairs: [(Node, Node)], _ tag: Tag = .implicit, _ style: Style = .any, _ mark: Mark? = nil, _ anchor: Anchor? = nil) { self.pairs = pairs.map { Pair($0.0, $0.1) } self.tag = tag self.style = style self.mark = mark + self.anchor = anchor } } @@ -166,7 +169,7 @@ extension Node.Mapping { index += 1 } } - return Node.Mapping(merge + pairs, tag, style) + return Node.Mapping(merge + pairs, tag, style, nil, anchor) } } diff --git a/Sources/Yams/Node.Scalar.swift b/Sources/Yams/Node.Scalar.swift index 662e8b4c..68640f0a 100644 --- a/Sources/Yams/Node.Scalar.swift +++ b/Sources/Yams/Node.Scalar.swift @@ -23,6 +23,8 @@ extension Node { public var style: Style /// The location for this node. public var mark: Mark? + /// The anchor for this node. + public weak var anchor: Anchor? /// The style to use when emitting a `Scalar`. public enum Style: UInt32 { @@ -48,11 +50,12 @@ extension Node { /// - parameter tag: This scalar's `Tag`. /// - parameter style: The style to use when emitting this `Scalar`. /// - parameter mark: This scalar's `Mark`. - public init(_ string: String, _ tag: Tag = .implicit, _ style: Style = .any, _ mark: Mark? = nil) { + public init(_ string: String, _ tag: Tag = .implicit, _ style: Style = .any, _ mark: Mark? = nil, _ anchor: Anchor? = nil) { self.string = string self.tag = tag self.style = style self.mark = mark + self.anchor = anchor } } diff --git a/Sources/Yams/Node.Sequence.swift b/Sources/Yams/Node.Sequence.swift index 571c9aee..c74f039e 100644 --- a/Sources/Yams/Node.Sequence.swift +++ b/Sources/Yams/Node.Sequence.swift @@ -18,6 +18,8 @@ extension Node { public var style: Style /// The location for this node. public var mark: Mark? + /// The anchor for this node. + public weak var anchor: Anchor? /// The style to use when emitting a `Sequence`. public enum Style: UInt32 { @@ -35,11 +37,12 @@ extension Node { /// - parameter tag: This sequence's `Tag`. /// - parameter style: The style to use when emitting this `Sequence`. /// - parameter mark: This sequence's `Mark`. - public init(_ nodes: [Node], _ tag: Tag = .implicit, _ style: Style = .any, _ mark: Mark? = nil) { + public init(_ nodes: [Node], _ tag: Tag = .implicit, _ style: Style = .any, _ mark: Mark? = nil, _ anchor: Anchor? = nil) { self.nodes = nodes self.tag = tag self.style = style self.mark = mark + self.anchor = anchor } } diff --git a/Sources/Yams/Node.swift b/Sources/Yams/Node.swift index 87728960..c848da43 100644 --- a/Sources/Yams/Node.swift +++ b/Sources/Yams/Node.swift @@ -16,6 +16,8 @@ public enum Node: Hashable { case mapping(Mapping) /// Sequence node. case sequence(Sequence) + /// Alias node. + case alias(Alias) } extension Node { @@ -24,8 +26,8 @@ extension Node { /// - parameter string: String value for this node. /// - parameter tag: Tag for this node. /// - parameter style: Style to use when emitting this node. - public init(_ string: String, _ tag: Tag = .implicit, _ style: Scalar.Style = .any) { - self = .scalar(.init(string, tag, style)) + public init(_ string: String, _ tag: Tag = .implicit, _ style: Scalar.Style = .any, _ anchor: Anchor? = nil) { + self = .scalar(.init(string, tag, style, nil, anchor)) } /// Create a `Node.mapping` with a sequence of node pairs, tag & scalar style. @@ -33,8 +35,8 @@ extension Node { /// - parameter pairs: Pairs of nodes to use for this node. /// - parameter tag: Tag for this node. /// - parameter style: Style to use when emitting this node. - public init(_ pairs: [(Node, Node)], _ tag: Tag = .implicit, _ style: Mapping.Style = .any) { - self = .mapping(.init(pairs, tag, style)) + public init(_ pairs: [(Node, Node)], _ tag: Tag = .implicit, _ style: Mapping.Style = .any, _ anchor: Anchor? = nil) { + self = .mapping(.init(pairs, tag, style, nil, anchor)) } /// Create a `Node.sequence` with a sequence of nodes, tag & scalar style. @@ -42,8 +44,8 @@ extension Node { /// - parameter nodes: Sequence of nodes to use for this node. /// - parameter tag: Tag for this node. /// - parameter style: Style to use when emitting this node. - public init(_ nodes: [Node], _ tag: Tag = .implicit, _ style: Sequence.Style = .any) { - self = .sequence(.init(nodes, tag, style)) + public init(_ nodes: [Node], _ tag: Tag = .implicit, _ style: Sequence.Style = .any, _ anchor: Anchor? = nil) { + self = .sequence(.init(nodes, tag, style, nil, anchor)) } } @@ -58,6 +60,7 @@ extension Node { case let .scalar(scalar): return scalar.resolvedTag case let .mapping(mapping): return mapping.resolvedTag case let .sequence(sequence): return sequence.resolvedTag + case let .alias(alias): return alias.resolvedTag } } @@ -67,6 +70,17 @@ extension Node { case let .scalar(scalar): return scalar.mark case let .mapping(mapping): return mapping.mark case let .sequence(sequence): return sequence.mark + case let .alias(alias): return alias.mark + } + } + + /// The anchor for this node. + public var anchor: Anchor? { + switch self { + case let .scalar(scalar): return scalar.anchor + case let .mapping(mapping): return mapping.anchor + case let .sequence(sequence): return sequence.anchor + case let .alias(alias): return alias.anchor } } @@ -139,7 +153,7 @@ extension Node { public subscript(node: Node) -> Node? { get { switch self { - case .scalar: return nil + case .scalar, .alias: return nil case let .mapping(mapping): return mapping[node] case let .sequence(sequence): @@ -150,7 +164,7 @@ extension Node { set { guard let newValue = newValue else { return } switch self { - case .scalar: return + case .scalar, .alias: return case .mapping(var mapping): mapping[node] = newValue self = .mapping(mapping) @@ -290,4 +304,38 @@ extension Node { } return false } + + func setting(anchor: Anchor) -> Self { + switch self { + case var .mapping(mapping): + mapping.anchor = anchor + return .mapping(mapping) + case var .sequence(sequence): + sequence.anchor = anchor + return .sequence(sequence) + case var .scalar(scalar): + scalar.anchor = anchor + return .scalar(scalar) + case var .alias(alias): + alias.anchor = anchor + return .alias(alias) + } + } + + func setting(tag: Tag) -> Self { + switch self { + case var .mapping(mapping): + mapping.tag = tag + return .mapping(mapping) + case var .sequence(sequence): + sequence.tag = tag + return .sequence(sequence) + case var .scalar(scalar): + scalar.tag = tag + return .scalar(scalar) + case var .alias(alias): + alias.tag = tag + return .alias(alias) + } + } } diff --git a/Sources/Yams/Parser.swift b/Sources/Yams/Parser.swift index 41a4d74a..233c7fd6 100644 --- a/Sources/Yams/Parser.swift +++ b/Sources/Yams/Parser.swift @@ -254,7 +254,9 @@ public final class Parser { // MARK: - Private Members - private var anchors = [String: Node]() + private var _anchorMap = [Anchor: Node]() + private var _anchorList = [Anchor]() + private var anchors: [Anchor: Node] { _anchorMap } private var parser = yaml_parser_t() private enum Buffer { @@ -263,6 +265,20 @@ public final class Parser { case utf16(Data) } private var buffer: Buffer + + // MARK: – Pivate Mutators + private func register(anchor: Anchor?, to node: Node) { + if let anchor { + _anchorList.append(anchor) + // We keep a list (not a set) of all anchors encountered + // because yaml anchors are allowed to shadow one another. + // + // The map will keep the latest reference as expected + // but without the list the map will release reference to + // one of the Anchor instances whenever duplicates are encountered. + _anchorMap[anchor] = node + } + } } // MARK: Implementation Details @@ -324,10 +340,9 @@ private extension Parser { } func loadScalar(from event: Event) throws -> Node { - let node = Node.scalar(.init(event.scalarValue, tag(event.scalarTag), event.scalarStyle, event.startMark)) - if let anchor = event.scalarAnchor { - anchors[anchor] = node - } + let anchor = event.scalarAnchor + let node = Node.scalar(.init(event.scalarValue, tag(event.scalarTag), event.scalarStyle, event.startMark, anchor)) + register(anchor: anchor, to: node) return node } @@ -338,10 +353,9 @@ private extension Parser { array.append(try loadNode(from: event)) event = try parse() } - let node = Node.sequence(.init(array, tag(firstEvent.sequenceTag), event.sequenceStyle, firstEvent.startMark)) - if let anchor = firstEvent.sequenceAnchor { - anchors[anchor] = node - } + let anchor = firstEvent.sequenceAnchor + let node = Node.sequence(.init(array, tag(firstEvent.sequenceTag), event.sequenceStyle, firstEvent.startMark, anchor)) + register(anchor: anchor, to: node) return node } @@ -355,10 +369,9 @@ private extension Parser { pairs.append((key, value)) event = try parse() } - let node = Node.mapping(.init(pairs, tag(firstEvent.mappingTag), event.mappingStyle, firstEvent.startMark)) - if let anchor = firstEvent.mappingAnchor { - anchors[anchor] = node - } + let anchor = firstEvent.mappingAnchor + let node = Node.mapping(.init(pairs, tag(firstEvent.mappingTag), event.mappingStyle, firstEvent.startMark, anchor)) + register(anchor: anchor, to: node) return node } @@ -378,13 +391,13 @@ private class Event { } // alias - var aliasAnchor: String? { - return string(from: event.data.alias.anchor) + var aliasAnchor: Anchor? { + return string(from: event.data.alias.anchor).map(Anchor.init(stringLiteral: )) } // scalar - var scalarAnchor: String? { - return string(from: event.data.scalar.anchor) + var scalarAnchor: Anchor? { + return string(from: event.data.scalar.anchor).map(Anchor.init(stringLiteral: )) } var scalarStyle: Node.Scalar.Style { // swiftlint:disable:next force_unwrapping @@ -405,8 +418,8 @@ private class Event { } // sequence - var sequenceAnchor: String? { - return string(from: event.data.sequence_start.anchor) + var sequenceAnchor: Anchor? { + return string(from: event.data.sequence_start.anchor).map(Anchor.init(stringLiteral: )) } var sequenceStyle: Node.Sequence.Style { // swiftlint:disable:next force_unwrapping @@ -418,8 +431,8 @@ private class Event { } // mapping - var mappingAnchor: String? { - return string(from: event.data.scalar.anchor) + var mappingAnchor: Anchor? { + return string(from: event.data.mapping_start.anchor).map(Anchor.init(stringLiteral: )) } var mappingStyle: Node.Mapping.Style { // swiftlint:disable:next force_unwrapping diff --git a/Sources/Yams/RedundancyAliasingStrategy.swift b/Sources/Yams/RedundancyAliasingStrategy.swift new file mode 100644 index 00000000..d802847e --- /dev/null +++ b/Sources/Yams/RedundancyAliasingStrategy.swift @@ -0,0 +1,117 @@ +// +// RedundancyAliasingStrategy.swift +// Yams +// +// Created by Adora Lynch on 8/15/24. +// Copyright (c) 2024 Yams. All rights reserved. +// + +import Foundation + +public enum RedundancyAliasingOutcome { + case anchor(Anchor) + case alias(Anchor) + case none +} + +/// A class-bound protocol which implements a strategy for detecting aliasable values in a YAML document. +/// Implementations should return RedundancyAliasingOutcome.anchor(...) for the first occurrence of a value. +/// Subsequent occurrences of the same value (where same-ness is defined by the implementation) should +/// return RedundancyAliasingOutcome.alias(...) where the contained Anchor has the same value as the previously +/// returned RedundancyAliasingOutcome.anchor(...). Its the identity of the Anchor values returned that ultimately +/// informs the YAML encoder when to use aliases. +/// N,B. It is essential that implementations release all references to Anchors which are created by this type +/// when releaseAnchorReferences() is called by the Encoder. After this call the implementation will no longer be +/// referenced by the Encoder and will itself be released. +public protocol RedundancyAliasingStrategy: AnyObject { + + /// Implementations should return RedundancyAliasingOutcome.anchor(...) for the first occurrence of a value. + /// Subsequent occurrences of the same value (where same-ness is defined by the implementation) should + /// return RedundancyAliasingOutcome.alias(...) where the contained Anchor has the same value as the previously + /// returned RedundancyAliasingOutcome.anchor(...). Its the identity of the Anchor values returned that ultimately + /// informs the YAML encoder when to use aliases. + func alias(for encodable: any Encodable) throws -> RedundancyAliasingOutcome + + /// It is essential that implementations release all references to Anchors which are created by this type + /// when releaseAnchorReferences() is called by the Encoder. After this call, the implementation will no longer be + /// referenced by the Encoder and will itself be released. + + func releaseAnchorReferences() throws +} + +/// An implementation of RedundancyAliasingStrategy that defines alias-ability by Hashable-Equality. +/// i.e. if two values are Hashable-Equal, they will be aliased in the resultant YML document. +public class HashableAliasingStrategy: RedundancyAliasingStrategy { + private var hashesToAliases: [AnyHashable: Anchor] = [:] + + let uniqueAliasProvider = UniqueAliasProvider() + + public init() {} + + public func alias(for encodable: any Encodable) throws -> RedundancyAliasingOutcome { + guard let hashable = encodable as? any Hashable & Encodable else { + return .none + } + return try alias(for: hashable) + } + + private func alias(for hashable: any Hashable & Encodable) throws -> RedundancyAliasingOutcome { + let anyHashable = AnyHashable(hashable) + if let existing = hashesToAliases[anyHashable] { + return .alias(existing) + } else { + let newAlias = uniqueAliasProvider.uniqueAlias(for: hashable) + hashesToAliases[anyHashable] = newAlias + return .anchor(newAlias) + } + } + + public func releaseAnchorReferences() throws { + hashesToAliases.removeAll() + } +} + +/// An implementation of RedundancyAliasingStrategy that defines alias-ability by the coded representation of the values. +/// i.e. if two values encode to exactly the same, they will be aliased in the resultant YML document even if the values themselves are of different types +public class StrictEncodableAliasingStrategy: RedundancyAliasingStrategy { + private var codedToAliases: [String: Anchor] = [:] + + let uniqueAliasProvider = UniqueAliasProvider() + + public init() {} + + private let encoder = YAMLEncoder() + + public func alias(for encodable: any Encodable) throws -> RedundancyAliasingOutcome { + let coded = try encoder.encode(encodable) + if let existing = codedToAliases[coded] { + return .alias(existing) + } else { + let newAlias = uniqueAliasProvider.uniqueAlias(for: encodable) + codedToAliases[coded] = newAlias + return .anchor(newAlias) + } + } + + public func releaseAnchorReferences() throws { + codedToAliases.removeAll() + } +} + +class UniqueAliasProvider { + private var counter = 0 + + func uniqueAlias(for encodable: any Encodable) -> Anchor { + if let anchorProviding = encodable as? YamlAnchorProviding, + let anchor = anchorProviding.yamlAnchor { + return anchor + } else { + counter += 1 + return Anchor(rawValue: String(counter)) + } + } +} + +extension CodingUserInfoKey { + internal static let redundancyAliasingStrategyKey = Self(rawValue: "redundancyAliasingStrategy")! +} diff --git a/Sources/Yams/Resolver.swift b/Sources/Yams/Resolver.swift index 40abb2da..4cf8d743 100644 --- a/Sources/Yams/Resolver.swift +++ b/Sources/Yams/Resolver.swift @@ -48,6 +48,8 @@ public final class Resolver { return resolveTag(of: mapping) case let .sequence(sequence): return resolveTag(of: sequence) + case let .alias(alias): + return resolveTag(of: alias) } } diff --git a/Sources/Yams/Tag.swift b/Sources/Yams/Tag.swift index fd2e1dbd..47e774a5 100644 --- a/Sources/Yams/Tag.swift +++ b/Sources/Yams/Tag.swift @@ -85,6 +85,24 @@ extension Tag: Hashable { } } +extension Tag: RawRepresentable { + public convenience init?(rawValue: String) { + self.init(stringLiteral: rawValue) + } + public var rawValue: String { + name.rawValue + } +} + +extension Tag: Codable {} + +extension Tag: ExpressibleByStringLiteral { + /// :nodoc: + public convenience init(stringLiteral value: String) { + self.init(.init(rawValue: value)) + } +} + extension Tag.Name: ExpressibleByStringLiteral { /// :nodoc: public init(stringLiteral value: String) { @@ -92,6 +110,8 @@ extension Tag.Name: ExpressibleByStringLiteral { } } +extension Tag.Name: Codable {} + // http://www.yaml.org/spec/1.2/spec.html#Schema extension Tag.Name { // Special diff --git a/Sources/Yams/YamlAnchorProviding.swift b/Sources/Yams/YamlAnchorProviding.swift new file mode 100644 index 00000000..dfc4e899 --- /dev/null +++ b/Sources/Yams/YamlAnchorProviding.swift @@ -0,0 +1,45 @@ +// +// YamlAnchorProviding.swift +// Yams +// +// Created by Adora Lynch on 8/15/24. +// Copyright (c) 2024 Yams. All rights reserved. +// + +import Foundation + +/// Types that conform to YamlAnchorProviding and Encodable can optionally dictate the name of +/// a yaml anchor when they are encoded with YAMLEncoder +public protocol YamlAnchorProviding { + var yamlAnchor: Anchor? { get } +} + +/// YamlAnchorCoding refines YamlAnchorProviding. +/// Types that conform to YamlAnchorCoding and Decodable can decode yaml anchors +/// from source documents into `Anchor` values for reference or modification in memory. +public protocol YamlAnchorCoding: YamlAnchorProviding { + var yamlAnchor: Anchor? { get set } +} + +internal extension Node { + static let anchorKeyNode: Self = .scalar(.init(YamlAnchorFunctionNameProvider().getName())) +} + +private final class YamlAnchorFunctionNameProvider: YamlAnchorProviding { + + fileprivate var functionName: StaticString? + + var yamlAnchor: Anchor? { + functionName = #function + return nil + } + + func getName() -> StaticString { + _ = yamlAnchor + return functionName! + } + + func getName() -> String { + String(describing: getName() as StaticString) + } +} diff --git a/Sources/Yams/YamlTagProviding.swift b/Sources/Yams/YamlTagProviding.swift new file mode 100644 index 00000000..0f2a2d63 --- /dev/null +++ b/Sources/Yams/YamlTagProviding.swift @@ -0,0 +1,43 @@ +// +// YamlTagProviding.swift +// +// +// Created by Adora Lynch on 9/5/24. +// Copyright (c) 2024 Yams. All rights reserved. +// + +/// Types that conform to YamlTagProviding and Encodable can optionally dictate the name of +/// a yaml tag when they are encoded with YAMLEncoder +public protocol YamlTagProviding { + var yamlTag: Tag? { get } +} + +/// YamlTagCoding refines YamlTagProviding. +/// Types that conform to YamlTagCoding and Decodable can decode yaml tags +/// from source documents into `Tag` values for reference or modification in memory. +public protocol YamlTagCoding: YamlTagProviding { + var yamlTag: Tag? { get set } +} + +internal extension Node { + static let tagKeyNode: Self = .scalar(.init(YamlTagFunctionNameProvider().getName())) +} + +private final class YamlTagFunctionNameProvider: YamlTagProviding { + + fileprivate var functionName: StaticString? + + var yamlTag: Tag? { + functionName = #function + return nil + } + + func getName() -> StaticString { + _ = yamlTag + return functionName! + } + + func getName() -> String { + String(describing: getName() as StaticString) + } +} diff --git a/Tests/YamsTests/AnchorCodingTests.swift b/Tests/YamsTests/AnchorCodingTests.swift new file mode 100644 index 00000000..b3ddc43b --- /dev/null +++ b/Tests/YamsTests/AnchorCodingTests.swift @@ -0,0 +1,298 @@ +// +// AnchorEncodingTests.swift +// Yams +// +// Created by Adora Lynch on 8/9/24. +// Copyright (c) 2024 Yams. All rights reserved. +// + +import XCTest +import Yams + +class AnchorCodingTests: XCTestCase { + + /// Test the encoding of a yaml anchor using a type that conforms to YamlAnchorProviding + func testYamlAnchorProviding_valuePresent() throws { + let simpleStruct = SimpleWithAnchor(nested: .init(stringValue: "it's a value"), intValue: 52) + + _testRoundTrip(of: simpleStruct, + expectedYAML:""" + &simple + nested: + stringValue: it's a value + intValue: 52 + + """ ) // ^ the Yams.Anchor is encoded as a yaml anchor + } + + /// Test the encoding of a a type that does not conform to YamlAnchorProviding but none the less declares a coding member with the same name + func testStringTypeAnchorName_valuePresent() throws { + let simpleStruct = SimpleWithStringTypeAnchorName(nested: .init(stringValue: "it's a value"), + intValue: 52, + yamlAnchor: "but typed as a string") + + _testRoundTrip(of: simpleStruct, + expectedYAML:""" + nested: + stringValue: it's a value + intValue: 52 + yamlAnchor: but typed as a string + + """ ) // ^ the member is _not_ treated as an anchor + } + + /// Nothing interesting happens when a type does not conform to YamlAnchorProviding none the less declares a coding member with the same name but that value is nil + func testStringTypeAnchorName_valueNotPresent() throws { + let expectedStruct = SimpleWithStringTypeAnchorName(nested: .init(stringValue: "it's a value"), + intValue: 52, + yamlAnchor: nil) + _testRoundTrip(of: expectedStruct, + expectedYAML: """ + nested: + stringValue: it's a value + intValue: 52 + + """) + } + + /// This test documents some undesirable behavior, but in an unlikely circumstance. + /// If the decoded type does not conform to YamlAnchorProviding it can still have a coding key called `yamlAnchor` + /// If Yams tries to decode such a type AND the document has a nil value for `yamlAnchor` AND the parent context is a mapping AND that mapping has an actual anchor (in the document) + /// THEN Yams wrongly tries to decode the anchor as the declared type of key `yamlAnchor`. + /// If that declared type can be decoded from a scalar string value (like String and RawRepresentable where RawValue == String) then the decoding will actually succeed. + /// Which effectively injects an unexpected value into the decoded type. + func testStringTypeAnchorName_withAnchorPresent_valueNil() throws { + let expectedStruct = SimpleWithStringTypeAnchorName(nested: .init(stringValue: "it's a value"), + intValue: 52, + yamlAnchor: nil) + let decoder = YAMLDecoder() + let data = """ + &AnActualAnchor + nested: + stringValue: it's a value + intValue: 52 + + """.data(using: .utf8)! + + let decodedStruct = try decoder.decode(SimpleWithStringTypeAnchorName.self, from: data) + + let fixBulletin = "YESS!!! YOU FIXED IT! See \(#file):\(#line) for explanation." + + // begin assertions of known-but-undesirable behavior + XCTAssertNotEqual(decodedStruct, expectedStruct, fixBulletin) // We wish this was equal + XCTAssertEqual(decodedStruct.yamlAnchor, "AnActualAnchor", fixBulletin) // we wish .yamlAnchor was nil + // end assertions of known-but-undesirable behavior + + + // Check the remainder of the properties that the above confusion did not involve + XCTAssertEqual(decodedStruct.nested, expectedStruct.nested) + XCTAssertEqual(decodedStruct.intValue, expectedStruct.intValue) + } +} + +class AnchorAliasingTests: XCTestCase { + + /// CYaml library does not detect identical values and automatically alias them. + func testCyamlDoesNotAutoAlias_noAnchor() throws { + let simpleNoAnchor = SimpleWithoutAnchor(nested: .init(stringValue: "it's a value"), intValue: 52) + let differentTypesOneAnchor = SimplePair(first: simpleNoAnchor, + second: simpleNoAnchor) + + _testRoundTrip(of: differentTypesOneAnchor, + expectedYAML:""" + first: + nested: + stringValue: it's a value + intValue: 52 + second: + nested: + stringValue: it's a value + intValue: 52 + + """ ) + } + + /// CYaml library does not detect identical values and automatically alias them even if the first occurrence has an anchor. + func testCyamlDoesNotAutoAlias_uniqueAnchor() throws { + let simpleStruct = SimpleWithAnchor(nested: .init(stringValue: "it's a value"), intValue: 52) + let simpleNoAnchor = SimpleWithoutAnchor(nested: .init(stringValue: "it's a value"), intValue: 52) + let differentTypesOneAnchor = SimplePair(first: simpleStruct, + second: simpleNoAnchor) + + _testRoundTrip(of: differentTypesOneAnchor, + expectedYAML:""" + first: &simple + nested: + stringValue: it's a value + intValue: 52 + second: + nested: + stringValue: it's a value + intValue: 52 + + """ ) + } + + /// CYaml library does not detect identical values and automatically alias them even if they have identical anchors. + // This one is not a shortcoming of CYaml. The yaml spec requires that nodes can shadow earlier anchors. + func testCyamlDoesNotAutoAlias_duplicateAnchor() throws { + let simpleStruct = SimpleWithAnchor(nested: .init(stringValue: "it's a value"), intValue: 52) + let duplicatedStructPair = SimplePair(first: simpleStruct, second: simpleStruct) + + _testRoundTrip(of: duplicatedStructPair, + expectedYAML:""" + first: &simple + nested: + stringValue: it's a value + intValue: 52 + second: &simple + nested: + stringValue: it's a value + intValue: 52 + + """ ) + } + + + /// 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 options = YAMLEncoder.Options(redundancyAliasingStrategy: HashableAliasingStrategy()) + _testRoundTrip(of: duplicatedStructArray, + with: options, + expectedYAML:""" + - &simple + nested: + stringValue: it's a value + intValue: 52 + - *simple + + """ ) + } + + /// 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 options = YAMLEncoder.Options(redundancyAliasingStrategy: HashableAliasingStrategy()) + _testRoundTrip(of: duplicatedStructArray, + with: options, + expectedYAML:""" + - &2 + nested: + stringValue: it's a value + intValue: 52 + - *2 + + """ ) + } + + /// 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 options = YAMLEncoder.Options(redundancyAliasingStrategy: HashableAliasingStrategy()) + _testRoundTrip(of: differentTypesOneAnchors, + with: options, + expectedYAML:""" + first: &simple + nested: &2 + stringValue: it's a value + intValue: &4 52 + second: + nested: *2 + intValue: *4 + + """ ) + } + + /// 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 options = YAMLEncoder.Options(redundancyAliasingStrategy: HashableAliasingStrategy()) + _testRoundTrip(of: differentTypesNoAnchors, + with: options, + expectedYAML:""" + first: + nested: &3 + stringValue: it's a value + intValue: &5 52 + second: + nested: *3 + intValue: *5 + + """ ) + } + + /// 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)) + + var options = YAMLEncoder.Options() + options.redundancyAliasingStrategy = StrictEncodableAliasingStrategy() + _testRoundTrip(of: differentTypesNoAnchors, + with: options, + expectedYAML:""" + first: &2 + nested: + stringValue: it's a value + intValue: 52 + second: *2 + + """ ) + } + + /// 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. + +fileprivate struct NestedStruct: Codable, Hashable { + let stringValue: String +} +fileprivate protocol SimpleProtocol: Codable, Hashable { + var nested: NestedStruct { get } + var intValue: Int { get } +} + +fileprivate struct SimpleWithAnchor: SimpleProtocol, YamlAnchorProviding { + let nested: NestedStruct + let intValue: Int + var yamlAnchor: Anchor? = "simple" +} + +fileprivate struct SimpleWithoutAnchor: SimpleProtocol { + let nested: NestedStruct + let intValue: Int +} + +fileprivate struct SimpleWithoutAnchor2: SimpleProtocol { + let nested: NestedStruct + let intValue: Int + var unrelatedValue: String? +} + +fileprivate struct SimpleWithStringTypeAnchorName: SimpleProtocol { + let nested: NestedStruct + let intValue: Int + var yamlAnchor: String? = "StringTypeAnchor" +} + + + diff --git a/Tests/YamsTests/AnchorTolerancesTests.swift b/Tests/YamsTests/AnchorTolerancesTests.swift new file mode 100644 index 00000000..60d126cb --- /dev/null +++ b/Tests/YamsTests/AnchorTolerancesTests.swift @@ -0,0 +1,162 @@ +// +// AnchorTolerancesTests.swift +// Yams +// +// Created by Adora Lynch on 9/18/24. +// Copyright (c) 2024 Yams. All rights reserved. +// + +import XCTest +import Yams + +class AnchorTolerancesTests: XCTestCase { + + struct Example: Codable, Hashable { + var myCustomAnchorDeclaration: Anchor + var extraneousValue: Int + } + + /// Any type that is Encodable and contains an `Anchor`value but with a coding key different from YamlAnchorProviding + /// will not encode to a yaml anchor + /// This may be unexpected + func testAnchorEncoding_undeclaredBehavior() throws { + let expectedYAML = """ + myCustomAnchorDeclaration: I-did-it-myyyyy-way + extraneousValue: 3 + + """ + + let value = Example(myCustomAnchorDeclaration: "I-did-it-myyyyy-way", + extraneousValue: 3) + + let encoder = YAMLEncoder() + let producedYAML = try encoder.encode(value) + XCTAssertEqual(producedYAML, expectedYAML, "Produced YAML not identical to expected YAML.") + } + + /// Any type that is Encodable and contains an `Anchor`value with the same coding key as YamlAnchorProviding + /// will encode to a yaml anchor even though the type does not conform to YamlAnchorProviding + /// This may be unexpected + func testAnchorEncoding_undeclaredBehavior_7() throws { + struct Example: Codable, Hashable { + var yamlAnchor: Anchor + var extraneousValue: Int + } + + let expectedYAML = """ + &I-did-it-myyyyy-way + extraneousValue: 3 + + """ + + let value = Example(yamlAnchor: "I-did-it-myyyyy-way", + extraneousValue: 3) + + let encoder = YAMLEncoder() + let producedYAML = try encoder.encode(value) + XCTAssertEqual(producedYAML, expectedYAML, "Produced YAML not identical to expected YAML.") + } + + /// Any type that is Decodable and contains an `Anchor` value but with a coding key different from YamlAnchorProviding + /// will not decode an anchor from the text representation. + /// In this case a key not found error will be thrown during decoding + /// This may be unexpected + func testAnchorDecoding_undeclaredBehavior_1() throws { + let sourceYAML = """ + &a-different-tag + extraneousValue: 3 + + """ + let decoder = YAMLDecoder() + XCTAssertThrowsError(try decoder.decode(Example.self, from: sourceYAML)) + // error is ^^ key not found, "myCustomAnchorDeclaration" + } + + /// Any type that is Decodable and contains an `Anchor` value but with a coding key different from YamlAnchorProviding + /// will not decode an anchor from the text representation. + /// In this case the decoding is successful and the anchor is respected by the parser. + /// This may be unexpected + func testAnchorDecoding_undeclaredBehavior_6() throws { + struct Example: Codable, Hashable { + var myCustomAnchorDeclaration: Anchor? + var extraneousValue: Int + } + let sourceYAML = """ + &a-different-tag + extraneousValue: 3 + + """ + + let expectedValue = Example(myCustomAnchorDeclaration: nil, + extraneousValue: 3) + + let decoder = YAMLDecoder() + let decodedValue = try decoder.decode(Example.self, from: sourceYAML) + XCTAssertEqual(decodedValue, expectedValue, "\(Example.self) did not round-trip to an equal value.") + } + + /// Any type that is Decodable and contains an `Anchor` value with the same coding key as YamlAnchorProviding + /// will decode an anchor from the text representation even though the type does not conform to YamlAnchorCoding + /// This may be unexpected + func testAnchorDecoding_undeclaredBehavior_8() throws { + struct Example: Codable, Hashable { + var yamlAnchor: Anchor? + var extraneousValue: Int + } + let sourceYAML = """ + &a-different-tag + extraneousValue: 3 + + """ + + let expectedValue = Example(yamlAnchor: "a-different-tag", + extraneousValue: 3) + + let decoder = YAMLDecoder() + let decodedValue = try decoder.decode(Example.self, from: sourceYAML) + XCTAssertEqual(decodedValue, expectedValue, "\(Example.self) did not round-trip to an equal value.") + } + + /// Any type that is Decodable and contains an `Anchor` value but with a coding key different from YamlAnchorProviding + /// will not decode an anchor from the text representation. + /// In this case the decoding is successful and the anchor is respected by the parser. + /// This is expected behavior, but in a strange situation. + func testAnchorDecoding_undeclaredBehavior_3() throws { + let sourceYAML = """ + &a-different-tag + extraneousValue: 3 + myCustomAnchorDeclaration: deliver-us-from-evil + + """ + let expectedValue = Example(myCustomAnchorDeclaration: "deliver-us-from-evil", + extraneousValue: 3) + + let decoder = YAMLDecoder() + let decodedValue = try decoder.decode(Example.self, from: sourceYAML) + XCTAssertEqual(decodedValue, expectedValue, "\(Example.self) did not round-trip to an equal value.") + + } + + /// Any type that is Decodable and contains an `Anchor` value but with a coding key different from YamlAnchorProviding + /// will not decode an anchor from the text representation. + /// In this case the decoding is successful even though and the `Anchor` was initialized with unsupported characters. + /// The anchor is respected by the parser. + /// This is expected behavior, but in a strange situation. + func testAnchorDecoding_undeclaredBehavior_2() throws { + let sourceYAML = """ + &a-different-tag + extraneousValue: 3 + myCustomAnchorDeclaration: "deliver us from |()evil" + + """ + + let expectedValue = Example(myCustomAnchorDeclaration: "deliver us from |()evil", + extraneousValue: 3) + + let decoder = YAMLDecoder() + let decodedValue = try decoder.decode(Example.self, from: sourceYAML) + XCTAssertEqual(decodedValue, expectedValue, "\(Example.self) did not round-trip to an equal value.") + + } + +} diff --git a/Tests/YamsTests/EncoderTests.swift b/Tests/YamsTests/EncoderTests.swift index e5a06dd2..284cfacf 100644 --- a/Tests/YamsTests/EncoderTests.swift +++ b/Tests/YamsTests/EncoderTests.swift @@ -405,35 +405,6 @@ class EncoderTests: XCTestCase { // swiftlint:disable:this type_body_length // MARK: - Helper Functions - private func _testRoundTrip(of value: T, - with options: YAMLEncoder.Options = .init(), - expectedYAML yamlString: String? = nil, - file: StaticString = #file, - line: UInt = #line) where T: Codable, T: Equatable { - do { - let encoder = YAMLEncoder() - encoder.options = options - let producedYAML = try encoder.encode(value) - - if let expectedYAML = yamlString { - XCTAssertEqual(producedYAML, expectedYAML, "Produced YAML not identical to expected YAML.", - file: (file), line: line) - } - - let decoder = YAMLDecoder() - 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) - - } 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 { - XCTFail("Failed to decode \(T.self) from YAML by error: \(error)", file: (file), line: line) - } catch { - XCTFail("Rout trip test of \(T.self) failed with error: \(error)", file: (file), line: line) - } - } - private func _testDecode(of type: T.Type, from string: String, expectedValue value: T?, @@ -468,6 +439,35 @@ class EncoderTests: XCTestCase { // swiftlint:disable:this type_body_length } } +internal func _testRoundTrip(of value: T, + with options: YAMLEncoder.Options = .init(), + expectedYAML yamlString: String? = nil, + file: StaticString = #file, + line: UInt = #line) where T: Codable, T: Equatable { + do { + let encoder = YAMLEncoder() + encoder.options = options + let producedYAML = try encoder.encode(value) + + if let expectedYAML = yamlString { + XCTAssertEqual(producedYAML, expectedYAML, "Produced YAML not identical to expected YAML.", + file: (file), line: line) + } + + let decoder = YAMLDecoder() + 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) + + } 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 { + XCTFail("Failed to decode \(T.self) from YAML by error: \(error)", file: (file), line: line) + } catch { + XCTFail("Rout trip test of \(T.self) failed with error: \(error)", file: (file), line: line) + } +} + // MARK: - Helper Global Functions public func expectEqual( _ expected: T, _ actual: T, diff --git a/Tests/YamsTests/TagCodingTests.swift b/Tests/YamsTests/TagCodingTests.swift new file mode 100644 index 00000000..f9bc55be --- /dev/null +++ b/Tests/YamsTests/TagCodingTests.swift @@ -0,0 +1,240 @@ +// +// TagCodingTests.swift +// Yams +// +// Created by Adora Lynch on 9/18/24. +// Copyright (c) 2024 Yams. All rights reserved. +// + +import XCTest +import Yams + +class TagCodingTests: XCTestCase { + + /// Test the encoding of a yaml tag using a type that conforms to YamlTagProviding + func testYamlTagProviding_valuePresent() throws { + let simpleStruct = SimpleWithTag(nested: .init(stringValue: "it's a value"), intValue: 52) + + _testRoundTrip(of: simpleStruct, + expectedYAML:""" + ! + nested: + stringValue: it's a value + intValue: 52 + + """ ) // ^ the Yams.Tag is encoded as a yaml tag + } + + /// Test the encoding of a a type that does not conform to YamlTagProviding but none the less declares a coding member with the same name + func testStringTypeTagName_valuePresent() throws { + let simpleStruct = SimpleWithStringTypeTagName(nested: .init(stringValue: "it's a value"), + intValue: 52, + yamlTag: "but typed as a string") + + _testRoundTrip(of: simpleStruct, + expectedYAML:""" + nested: + stringValue: it's a value + intValue: 52 + yamlTag: but typed as a string + + """ ) // ^ the member is _not_ treated as an tag + } + + /// Nothing interesting happens when a type does not conform to YamlTagProviding none the less declares a coding member with the same name but that value is nil + func testStringTypeTagName_valueNotPresent() throws { + let expectedStruct = SimpleWithStringTypeTagName(nested: .init(stringValue: "it's a value"), + intValue: 52, + yamlTag: nil) + _testRoundTrip(of: expectedStruct, + expectedYAML: """ + nested: + stringValue: it's a value + intValue: 52 + + """) + } + + /// This test documents some undesirable behavior, but in an unlikely circumstance. + /// If the decoded type does not conform to YamlTagProviding it can still have a coding key called `yamlTag` + /// If Yams tries to decode such a type AND the document has a nil value for `yamlTag` AND the parent context is a mapping AND that mapping has an actual tag (in the document) + /// THEN Yams wrongly tries to decode the tag as the declared type of key `yamlTag`. + /// If that declared type can be decoded from a scalar string value (like String and RawRepresentable where RawValue == String) then the decoding will actually succeed. + /// Which effectively injects an unexpected value into the decoded type. + func testStringTypeTagName_withTagPresent_valueNil() throws { + let expectedStruct = SimpleWithStringTypeTagName(nested: .init(stringValue: "it's a value"), + intValue: 52, + yamlTag: nil) + let decoder = YAMLDecoder() + let data = """ + ! + nested: + stringValue: it's a value + intValue: 52 + + """.data(using: .utf8)! + + let decodedStruct = try decoder.decode(SimpleWithStringTypeTagName.self, from: data) + + let fixBulletin = "YESS!!! YOU FIXED IT! See \(#file):\(#line) for explanation." + + // begin assertions of known-but-undesirable behavior + XCTAssertNotEqual(decodedStruct, expectedStruct, fixBulletin) // We wish this was equal + XCTAssertEqual(decodedStruct.yamlTag, "An:Actual:Tag", fixBulletin) // we wish .yamlTag was nil + // end assertions of known-but-undesirable behavior + + + // Check the remainder of the properties that the above confusion did not involve + XCTAssertEqual(decodedStruct.nested, expectedStruct.nested) + XCTAssertEqual(decodedStruct.intValue, expectedStruct.intValue) + } +} + +class TagWithAnchorCodingTests: XCTestCase { + + /// If types conform to YamlTagProviding and are Hashable-Equal then HashableAliasingStrategy aliases them + func testEncoderAutoAlias_Hashable_duplicateValue_commonTag() throws { + let simpleStruct = SimpleWithTag(nested: .init(stringValue: "it's a value"), intValue: 52) + let duplicatedStructArray = [simpleStruct, simpleStruct] + + let options = YAMLEncoder.Options(redundancyAliasingStrategy: HashableAliasingStrategy()) + _testRoundTrip(of: duplicatedStructArray, + with: options, + expectedYAML:""" + - &2 ! + nested: + stringValue: it's a value + intValue: 52 + - *2 + + """ ) + } + + /// If types conform to YamlTagProviding 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_uniqueTag() throws { + let differentTypesOneTags = SimplePair(first: SimpleWithTag(nested: .init(stringValue: "it's a value"), intValue: 52), + second: SimpleWithoutTag(nested: .init(stringValue: "it's a value"), intValue: 52)) + + let options = YAMLEncoder.Options(redundancyAliasingStrategy: HashableAliasingStrategy()) + _testRoundTrip(of: differentTypesOneTags, + with: options, + expectedYAML:""" + first: ! + nested: &3 + stringValue: it's a value + intValue: &5 52 + second: + nested: *3 + intValue: *5 + + """ ) + } + + /// If types conform to YamlTagProviding can declare to have the same tag and still be 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_distinctValues_commonTag() throws { + let differentTypesOneTags = SimplePair(first: SimpleWithTag(nested: .init(stringValue: "it's a value"), intValue: 52), + second: SimpleWithTag2(nested: .init(stringValue: "it's a value"), intValue: 52)) + + let options = YAMLEncoder.Options(redundancyAliasingStrategy: HashableAliasingStrategy()) + _testRoundTrip(of: differentTypesOneTags, + with: options, + expectedYAML:""" + first: ! + nested: &3 + stringValue: it's a value + intValue: &5 52 + second: ! + nested: *3 + intValue: *5 + + """ ) + } + + /// If different types conform to YamlTagProviding they can declare to have the same tag and further, have exactly the same encoded representation. + /// In thisi case StrictEncodableAliasingStrategy will still alias them even though they are encoded and decoded from different types. + func testEncoderAutoAlias_StrictEncodable_distinctValues_commonTag() throws { + let differentTypesOneTags = SimplePair(first: SimpleWithTag(nested: .init(stringValue: "it's a value"), intValue: 52), + second: SimpleWithTag2(nested: .init(stringValue: "it's a value"), intValue: 52)) + + var options = YAMLEncoder.Options() + options.redundancyAliasingStrategy = StrictEncodableAliasingStrategy() + _testRoundTrip(of: differentTypesOneTags, + with: options, + expectedYAML:""" + first: &2 ! + nested: + stringValue: it's a value + intValue: 52 + second: *2 + + """ ) + } + + /// If types conform to YamlTagProviding and YamlAnchorProviding, both are respected. + func testEncoderAutoAlias_Hashable_commonTagAndAnchor() throws { + let simpleStruct = SimpleWithTagAndAnchor(nested: .init(stringValue: "it's a value"), intValue: 52) + let duplicatedStructArray = [simpleStruct, simpleStruct] + + let options = YAMLEncoder.Options(redundancyAliasingStrategy: HashableAliasingStrategy()) + _testRoundTrip(of: duplicatedStructArray, + with: options, + expectedYAML:""" + - &simple-Anchor ! + nested: + stringValue: it's a value + intValue: 52 + - *simple-Anchor + + """ ) + } + + /// A type used to contain values used during testing + private struct SimplePair: Hashable, Codable { + let first: First + let second: Second + } + +} +// MARK: - Types used for Tag encoding tests. + +fileprivate struct NestedStruct: Codable, Hashable { + let stringValue: String +} +fileprivate protocol SimpleProtocol: Codable, Hashable { + var nested: NestedStruct { get } + var intValue: Int { get } +} + +fileprivate struct SimpleWithTag: SimpleProtocol, YamlTagProviding { + let nested: NestedStruct + let intValue: Int + var yamlTag: Tag? = "simple" +} + +fileprivate struct SimpleWithTag2: SimpleProtocol, YamlTagProviding { + let nested: NestedStruct + let intValue: Int + var yamlTag: Tag? = "simple" +} + +fileprivate struct SimpleWithoutTag: SimpleProtocol { + let nested: NestedStruct + let intValue: Int +} + +fileprivate struct SimpleWithStringTypeTagName: SimpleProtocol { + let nested: NestedStruct + let intValue: Int + var yamlTag: String? = "StringTypeTag" +} + +fileprivate struct SimpleWithTagAndAnchor: SimpleProtocol, YamlTagProviding, YamlAnchorProviding { + let nested: NestedStruct + let intValue: Int + var yamlTag: Tag? = "simple:Tag" + var yamlAnchor: Anchor? = "simple-Anchor" +} + + diff --git a/Tests/YamsTests/TagTolerancesTests.swift b/Tests/YamsTests/TagTolerancesTests.swift new file mode 100644 index 00000000..62a73e1a --- /dev/null +++ b/Tests/YamsTests/TagTolerancesTests.swift @@ -0,0 +1,179 @@ +// +// TagTolerancesTests.swift +// Yams +// +// Created by Adora Lynch on 9/18/24. +// Copyright (c) 2024 Yams. All rights reserved. +// + +import XCTest +import Yams + +class TagTolerancesTests: XCTestCase { + + struct Example: Codable, Hashable { + var myCustomTagDeclaration: Tag + var extraneousValue: Int + } + + /// Any type that is Encodable and contains an `Tag`value but with a coding key different from YamlTagProviding + /// will not encode to a yaml tag + /// This may be unexpected + func testTagEncoding_undeclaredBehavior() throws { + let expectedYAML = """ + myCustomTagDeclaration: I-did-it-myyyyy-way + extraneousValue: 3 + + """ + + let value = Example(myCustomTagDeclaration: "I-did-it-myyyyy-way", + extraneousValue: 3) + + let encoder = YAMLEncoder() + let producedYAML = try encoder.encode(value) + XCTAssertEqual(producedYAML, expectedYAML, "Produced YAML not identical to expected YAML.") + } + + /// Any type that is Encodable and contains an `Tag`value with the same coding key as YamlTagProviding + /// will encode to a yaml tag even though the type does not conform to YamlTagProviding + /// This may be unexpected + func testTagEncoding_undeclaredBehavior_7() throws { + struct Example: Codable, Hashable { + var yamlTag: Tag + var extraneousValue: Int + } + let expectedYAML = """ + ! + extraneousValue: 3 + + """ + + let value = Example(yamlTag: "I-did-it-myyyyy-way", + extraneousValue: 3) + + let encoder = YAMLEncoder() + let producedYAML = try encoder.encode(value) + XCTAssertEqual(producedYAML, expectedYAML, "Produced YAML not identical to expected YAML.") + } + + /// Tags are oddly permissive, but some characters do get escaped + /// This may be unexpected + func testTagEncoding_undeclaredBehavior_4() throws { + struct Example: Codable, Hashable, YamlTagProviding { + var yamlTag: Tag? + var extraneousValue: Int + } + + let expectedYAML = """ + ! + extraneousValue: 3 + + """ + + let value = Example(yamlTag: "I-did-it-[]-*-|-!-()way", + extraneousValue: 3) + + let encoder = YAMLEncoder() + let producedYAML = try encoder.encode(value) + XCTAssertEqual(producedYAML, expectedYAML, "Produced YAML not identical to expected YAML.") + } + + /// Any type that is Decodable and contains an `Tag` value but with a coding key different from YamlTagProviding + /// will not decode an tag from the text representation. + /// In this case a key not found error will be thrown during decoding + /// This may be unexpected + func testTagDecoding_undeclaredBehavior_1() throws { + let sourceYAML = """ + ! + extraneousValue: 3 + + """ + let decoder = YAMLDecoder() + XCTAssertThrowsError(try decoder.decode(Example.self, from: sourceYAML)) + // error is ^^ key not found, "myCustomTagDeclaration" + } + + /// Any type that is Decodable and contains an `Tag` value but with a coding key different from YamlTagProviding + /// will not decode an tag from the text representation. + /// This may be unexpected + func testTagDecoding_undeclaredBehavior_6() throws { + struct Example: Codable, Hashable { + var myCustomTagDeclaration: Tag? + var extraneousValue: Int + } + let sourceYAML = """ + ! + extraneousValue: 3 + + """ + + let expectedValue = Example(myCustomTagDeclaration: nil, + extraneousValue: 3) + + let decoder = YAMLDecoder() + let decodedValue = try decoder.decode(Example.self, from: sourceYAML) + XCTAssertEqual(decodedValue, expectedValue, "\(Example.self) did not round-trip to an equal value.") + } + + /// Any type that is Decodable and contains an `Tag` value with the same coding key as YamlTagProviding + /// will decode an tag from the text representatio even though the type does not conform to YamlTagCoding. + /// This may be unexpected + func testTagDecoding_undeclaredBehavior_8() throws { + struct Example: Codable, Hashable { + var yamlTag: Tag? + var extraneousValue: Int + } + let sourceYAML = """ + ! + extraneousValue: 3 + + """ + + let expectedValue = Example(yamlTag: "a-different-tag", + extraneousValue: 3) + + let decoder = YAMLDecoder() + let decodedValue = try decoder.decode(Example.self, from: sourceYAML) + XCTAssertEqual(decodedValue, expectedValue, "\(Example.self) did not round-trip to an equal value.") + } + + /// Any type that is Decodable and contains an `Tag` value but with a coding key different from YamlTagProviding + /// will not decode an tag from the text representation. + /// This is expected behavior, but in a strange situation. + func testTagDecoding_undeclaredBehavior_3() throws { + let sourceYAML = """ + ! + extraneousValue: 3 + myCustomTagDeclaration: deliver-us-from-evil + + """ + let expectedValue = Example(myCustomTagDeclaration: "deliver-us-from-evil", + extraneousValue: 3) + + let decoder = YAMLDecoder() + let decodedValue = try decoder.decode(Example.self, from: sourceYAML) + XCTAssertEqual(decodedValue, expectedValue, "\(Example.self) did not round-trip to an equal value.") + + } + + /// Any type that is Decodable and contains an `Tag` value but with a coding key different from YamlTagProviding + /// will not decode an tag from the text representation. + /// This is expected behavior, but in a strange situation. + func testTagDecoding_undeclaredBehavior_2() throws { + let sourceYAML = """ + ! + extraneousValue: 3 + myCustomTagDeclaration: "deliver us from |()evil" + + """ + + let expectedValue = Example(myCustomTagDeclaration: "deliver us from |()evil", + extraneousValue: 3) + + let decoder = YAMLDecoder() + let decodedValue = try decoder.decode(Example.self, from: sourceYAML) + XCTAssertEqual(decodedValue, expectedValue, "\(Example.self) did not round-trip to an equal value.") + + } + +} From cd4bc612ce4ed62642111e04f6b3c4cbbf4789f9 Mon Sep 17 00:00:00 2001 From: Adora Lynch Date: Wed, 9 Oct 2024 15:29:33 -0400 Subject: [PATCH 2/7] Inform the .xcodeproj of new files and fix two tests in UTF16 mode. --- Sources/Yams/Parser.swift | 2 +- Tests/YamsTests/AnchorCodingTests.swift | 2 +- Tests/YamsTests/TagCodingTests.swift | 2 +- Yams.xcodeproj/project.pbxproj | 44 +++++++++++++++++++++++-- 4 files changed, 45 insertions(+), 5 deletions(-) diff --git a/Sources/Yams/Parser.swift b/Sources/Yams/Parser.swift index 233c7fd6..948c03be 100644 --- a/Sources/Yams/Parser.swift +++ b/Sources/Yams/Parser.swift @@ -140,7 +140,7 @@ public final class Parser { }() /// The equivalent `Swift.Encoding` value for `self`. - internal var swiftStringEncoding: String.Encoding { + public var swiftStringEncoding: String.Encoding { switch self { case .utf8: return .utf8 diff --git a/Tests/YamsTests/AnchorCodingTests.swift b/Tests/YamsTests/AnchorCodingTests.swift index b3ddc43b..24d89cba 100644 --- a/Tests/YamsTests/AnchorCodingTests.swift +++ b/Tests/YamsTests/AnchorCodingTests.swift @@ -72,7 +72,7 @@ class AnchorCodingTests: XCTestCase { stringValue: it's a value intValue: 52 - """.data(using: .utf8)! + """.data(using: decoder.encoding.swiftStringEncoding)! let decodedStruct = try decoder.decode(SimpleWithStringTypeAnchorName.self, from: data) diff --git a/Tests/YamsTests/TagCodingTests.swift b/Tests/YamsTests/TagCodingTests.swift index f9bc55be..1b3e0e6f 100644 --- a/Tests/YamsTests/TagCodingTests.swift +++ b/Tests/YamsTests/TagCodingTests.swift @@ -72,7 +72,7 @@ class TagCodingTests: XCTestCase { stringValue: it's a value intValue: 52 - """.data(using: .utf8)! + """.data(using: decoder.encoding.swiftStringEncoding)! let decodedStruct = try decoder.decode(SimpleWithStringTypeTagName.self, from: data) diff --git a/Yams.xcodeproj/project.pbxproj b/Yams.xcodeproj/project.pbxproj index a69ec6e4..f6cc6ca1 100644 --- a/Yams.xcodeproj/project.pbxproj +++ b/Yams.xcodeproj/project.pbxproj @@ -34,6 +34,16 @@ 6CF0253A1E9D12680061FB47 /* MarkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CF025391E9D12680061FB47 /* MarkTests.swift */; }; 6CF6CE091E0E3B1000CB87D4 /* PerformanceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CF6CE081E0E3B1000CB87D4 /* PerformanceTests.swift */; }; 8FA807DC24B250EF0082215D /* TopLevelDecoderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FA807DB24B250EF0082215D /* TopLevelDecoderTests.swift */; }; + 8FBD7F7F2CB70C8900271BB9 /* Node.Alias.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FBD7F7B2CB70C8900271BB9 /* Node.Alias.swift */; }; + 8FBD7F802CB70C8900271BB9 /* YamlAnchorProviding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FBD7F7D2CB70C8900271BB9 /* YamlAnchorProviding.swift */; }; + 8FBD7F812CB70C8900271BB9 /* YamlTagProviding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FBD7F7E2CB70C8900271BB9 /* YamlTagProviding.swift */; }; + 8FBD7F822CB70C8900271BB9 /* RedundancyAliasingStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FBD7F7C2CB70C8900271BB9 /* RedundancyAliasingStrategy.swift */; }; + 8FBD7F832CB70C8900271BB9 /* Anchor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FBD7F7A2CB70C8900271BB9 /* Anchor.swift */; }; + 8FBD7F892CB70CFB00271BB9 /* NodeDecoderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FBD7F862CB70CFB00271BB9 /* NodeDecoderTests.swift */; }; + 8FBD7F8A2CB70CFB00271BB9 /* AnchorCodingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FBD7F842CB70CFB00271BB9 /* AnchorCodingTests.swift */; }; + 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 */; }; 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 */; }; @@ -87,6 +97,16 @@ 6CF6CE071E0E3A5900CB87D4 /* Fixtures */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Fixtures; sourceTree = ""; }; 6CF6CE081E0E3B1000CB87D4 /* PerformanceTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PerformanceTests.swift; sourceTree = ""; }; 8FA807DB24B250EF0082215D /* TopLevelDecoderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopLevelDecoderTests.swift; sourceTree = ""; }; + 8FBD7F7A2CB70C8900271BB9 /* Anchor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Anchor.swift; sourceTree = ""; }; + 8FBD7F7B2CB70C8900271BB9 /* Node.Alias.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Node.Alias.swift; sourceTree = ""; }; + 8FBD7F7C2CB70C8900271BB9 /* RedundancyAliasingStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RedundancyAliasingStrategy.swift; sourceTree = ""; }; + 8FBD7F7D2CB70C8900271BB9 /* YamlAnchorProviding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YamlAnchorProviding.swift; sourceTree = ""; }; + 8FBD7F7E2CB70C8900271BB9 /* YamlTagProviding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YamlTagProviding.swift; sourceTree = ""; }; + 8FBD7F842CB70CFB00271BB9 /* AnchorCodingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnchorCodingTests.swift; sourceTree = ""; }; + 8FBD7F852CB70CFB00271BB9 /* AnchorTolerancesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnchorTolerancesTests.swift; sourceTree = ""; }; + 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 = ""; }; 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 = ""; }; @@ -131,21 +151,26 @@ OBJ_21 /* Yams */ = { isa = PBXGroup; children = ( + 8FBD7F7A2CB70C8900271BB9 /* Anchor.swift */, 6C0D2A351E0A934B00C45545 /* Constructor.swift */, 6C4AF31E1EBE14A1008775BC /* Decoder.swift */, 6CE603971E13502E00A13D8D /* Emitter.swift */, 6C788A021EB876C4005386F0 /* Encoder.swift */, 6CF025371E9CF4380061FB47 /* Mark.swift */, 6C6834C71E0281880047B4D1 /* Node.swift */, - 6C0409AB1E607E9900C95D83 /* Node.Scalar.swift */, + 8FBD7F7B2CB70C8900271BB9 /* Node.Alias.swift */, 6C0409A71E602E9A00C95D83 /* Node.Mapping.swift */, + 6C0409AB1E607E9900C95D83 /* Node.Scalar.swift */, 6C0409A91E6033DF00C95D83 /* Node.Sequence.swift */, 6C6834CB1E0283980047B4D1 /* Parser.swift */, + 8FBD7F7C2CB70C8900271BB9 /* RedundancyAliasingStrategy.swift */, 6CC2E33E1E22347B00F62269 /* Representer.swift */, 6C6834D21E02B9760047B4D1 /* Resolver.swift */, 6C4A22061DF8553C002A0206 /* String+Yams.swift */, 6C6834C91E0281D90047B4D1 /* Tag.swift */, + 8FBD7F7D2CB70C8900271BB9 /* YamlAnchorProviding.swift */, OBJ_22 /* YamlError.swift */, + 8FBD7F7E2CB70C8900271BB9 /* YamlTagProviding.swift */, 6CBAEE191E3839500021BF87 /* Yams.h */, ); name = Yams; @@ -164,17 +189,22 @@ OBJ_24 /* YamsTests */ = { isa = PBXGroup; children = ( - 6CF6CE071E0E3A5900CB87D4 /* Fixtures */, + 8FBD7F842CB70CFB00271BB9 /* AnchorCodingTests.swift */, + 8FBD7F852CB70CFB00271BB9 /* AnchorTolerancesTests.swift */, 6C0488ED1E0CBD56006F9F80 /* ConstructorTests.swift */, 6C0A00D41E152D6200222704 /* EmitterTests.swift */, 6C788A001EB87232005386F0 /* EncoderTests.swift */, + 6CF6CE071E0E3A5900CB87D4 /* Fixtures */, 6CF025391E9D12680061FB47 /* MarkTests.swift */, + 8FBD7F862CB70CFB00271BB9 /* NodeDecoderTests.swift */, 6C3C90B81E0FFB6B009DEFE8 /* NodeTests.swift */, 6CF6CE081E0E3B1000CB87D4 /* PerformanceTests.swift */, 6C78C5631E29B1CE0096215F /* RepresenterTests.swift */, 6C6834D41E02BC1F0047B4D1 /* ResolverTests.swift */, 6C6834D01E0297390047B4D1 /* SpecTests.swift */, 6C4A22081DF855BB002A0206 /* StringTests.swift */, + 8FBD7F872CB70CFB00271BB9 /* TagCodingTests.swift */, + 8FBD7F882CB70CFB00271BB9 /* TagTolerancesTests.swift */, 6C0488EB1E0BE113006F9F80 /* TestHelper.swift */, 8FA807DB24B250EF0082215D /* TopLevelDecoderTests.swift */, OBJ_25 /* YamlErrorTests.swift */, @@ -359,6 +389,11 @@ 6C4AF3201EBE1705008775BC /* Decoder.swift in Sources */, 6C0409AC1E607E9900C95D83 /* Node.Scalar.swift in Sources */, 6C4A22071DF8553C002A0206 /* String+Yams.swift in Sources */, + 8FBD7F7F2CB70C8900271BB9 /* Node.Alias.swift in Sources */, + 8FBD7F802CB70C8900271BB9 /* YamlAnchorProviding.swift in Sources */, + 8FBD7F812CB70C8900271BB9 /* YamlTagProviding.swift in Sources */, + 8FBD7F822CB70C8900271BB9 /* RedundancyAliasingStrategy.swift in Sources */, + 8FBD7F832CB70C8900271BB9 /* Anchor.swift in Sources */, E8EDB8891DE2181B0062268D /* parser.c in Sources */, 6CF025381E9CF4380061FB47 /* Mark.swift in Sources */, OBJ_50 /* YamlError.swift in Sources */, @@ -376,6 +411,11 @@ 6CF6CE091E0E3B1000CB87D4 /* PerformanceTests.swift in Sources */, 6C4A22091DF855BB002A0206 /* StringTests.swift in Sources */, 6C0488EC1E0BE113006F9F80 /* TestHelper.swift in Sources */, + 8FBD7F892CB70CFB00271BB9 /* NodeDecoderTests.swift in Sources */, + 8FBD7F8A2CB70CFB00271BB9 /* AnchorCodingTests.swift in Sources */, + 8FBD7F8B2CB70CFB00271BB9 /* AnchorTolerancesTests.swift in Sources */, + 8FBD7F8C2CB70CFB00271BB9 /* TagTolerancesTests.swift in Sources */, + 8FBD7F8D2CB70CFB00271BB9 /* TagCodingTests.swift in Sources */, 6C78C5651E29B27D0096215F /* RepresenterTests.swift in Sources */, 6C3C90B91E0FFB6B009DEFE8 /* NodeTests.swift in Sources */, 6C0A00D51E152D6200222704 /* EmitterTests.swift in Sources */, From 8f1409b94061daba73a28cb260082030d4c2f5ca Mon Sep 17 00:00:00 2001 From: Adora Lynch Date: Thu, 10 Oct 2024 19:26:41 -0400 Subject: [PATCH 3/7] Inform CMakeLists about new source files. --- Sources/Yams/CMakeLists.txt | 7 ++++++- Tests/YamsTests/CMakeLists.txt | 4 ++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/Sources/Yams/CMakeLists.txt b/Sources/Yams/CMakeLists.txt index 0633b699..0bababc9 100644 --- a/Sources/Yams/CMakeLists.txt +++ b/Sources/Yams/CMakeLists.txt @@ -1,20 +1,25 @@ add_library(Yams + Anchor.swift Constructor.swift Decoder.swift Emitter.swift Encoder.swift Mark.swift + Node.Alias.swift Node.Mapping.swift Node.Scalar.swift Node.Sequence.swift Node.swift Parser.swift + RedundancyAliasingStrategy.swift Representer.swift Resolver.swift String+Yams.swift Tag.swift - YamlError.swift) + YamlAnchorProviding.swift + YamlError.swift + YamlTagProviding.swift) target_compile_definitions(Yams PRIVATE SWIFT_PACKAGE) target_compile_options(Yams PRIVATE diff --git a/Tests/YamsTests/CMakeLists.txt b/Tests/YamsTests/CMakeLists.txt index daef104e..8b223cac 100644 --- a/Tests/YamsTests/CMakeLists.txt +++ b/Tests/YamsTests/CMakeLists.txt @@ -1,4 +1,6 @@ add_library(YamsTests + AnchorCodingTests.swift + AnchorTolerancesTests.swift ConstructorTests.swift EmitterTests.swift EncoderTests.swift @@ -9,6 +11,8 @@ add_library(YamsTests ResolverTests.swift SpecTests.swift StringTests.swift + TagCodingTests.swift + TagTolerancesTests.swift TestHelper.swift TopLevelDecoderTests.swift YamlErrorTests.swift) From 27afe7f0e98266c61f15fe4280f519c4bc9b0022 Mon Sep 17 00:00:00 2001 From: Adora Lynch Date: Fri, 11 Oct 2024 13:37:10 -0400 Subject: [PATCH 4/7] Adress Swiftlint errors --- Sources/Yams/Anchor.swift | 10 +- Sources/Yams/Constructor.swift | 18 +- Sources/Yams/Decoder.swift | 14 +- Sources/Yams/Emitter.swift | 18 +- Sources/Yams/Encoder.swift | 5 +- Sources/Yams/Node.Alias.swift | 4 +- Sources/Yams/Node.Mapping.swift | 6 +- Sources/Yams/Node.Scalar.swift | 6 +- Sources/Yams/Node.Sequence.swift | 6 +- Sources/Yams/Node.swift | 21 +- Sources/Yams/Parser.swift | 20 +- Sources/Yams/RedundancyAliasingStrategy.swift | 35 +- Sources/Yams/YamlAnchorProviding.swift | 10 +- Sources/Yams/YamlTagProviding.swift | 8 +- Tests/YamsTests/AnchorCodingTests.swift | 332 ++++++++++-------- Tests/YamsTests/AnchorTolerancesTests.swift | 123 +++---- Tests/YamsTests/EncoderTests.swift | 9 +- Tests/YamsTests/TagCodingTests.swift | 234 ++++++------ Tests/YamsTests/TagTolerancesTests.swift | 81 ++--- 19 files changed, 515 insertions(+), 445 deletions(-) diff --git a/Sources/Yams/Anchor.swift b/Sources/Yams/Anchor.swift index 6e63d256..30294ad5 100644 --- a/Sources/Yams/Anchor.swift +++ b/Sources/Yams/Anchor.swift @@ -8,23 +8,22 @@ import Foundation public final class Anchor: RawRepresentable, ExpressibleByStringLiteral, Codable, Hashable { - + public static let permittedCharacters = CharacterSet.lowercaseLetters .union(.uppercaseLetters) .union(.decimalDigits) .union(.init(charactersIn: "-_")) - + public static func is_cyamlAlpha(_ string: String) -> Bool { Anchor.permittedCharacters.isSuperset(of: .init(charactersIn: string)) } - public let rawValue: String - + public init(rawValue: String) { self.rawValue = rawValue } - + public init(stringLiteral value: String) { rawValue = value } @@ -33,4 +32,3 @@ public final class Anchor: RawRepresentable, ExpressibleByStringLiteral, Codable extension Anchor: CustomStringConvertible { public var description: String { rawValue } } - diff --git a/Sources/Yams/Constructor.swift b/Sources/Yams/Constructor.swift index e93ed2c5..ae56777b 100644 --- a/Sources/Yams/Constructor.swift +++ b/Sources/Yams/Constructor.swift @@ -48,13 +48,13 @@ public final class Constructor { if let method = mappingMap[node.tag.name], let result = method(mapping) { return result } - return [AnyHashable: Any]._construct_mapping(from: mapping) + return [AnyHashable: Any].private_construct_mapping(from: mapping) case .sequence(let sequence): if let method = sequenceMap[node.tag.name], let result = method(sequence) { return result } return [Any].construct_seq(from: sequence) - case .alias(_): + case .alias: preconditionFailure("Aliases should be resolved before construction") } } @@ -272,7 +272,7 @@ extension ScalarConstructible where Self: FloatingPoint & SexagesimalConvertible } private extension FixedWidthInteger where Self: SexagesimalConvertible { - static func _construct(from scalar: Node.Scalar) -> Self? { + static func private_construct(from scalar: Node.Scalar) -> Self? { guard scalar.style == .any || scalar.style == .plain else { return nil } @@ -317,7 +317,7 @@ extension Int: ScalarConstructible { /// /// - returns: An instance of `Int`, if one was successfully extracted from the scalar. public static func construct(from scalar: Node.Scalar) -> Int? { - return _construct(from: scalar) + return private_construct(from: scalar) } } @@ -330,7 +330,7 @@ extension UInt: ScalarConstructible { /// /// - returns: An instance of `UInt`, if one was successfully extracted from the scalar. public static func construct(from scalar: Node.Scalar) -> UInt? { - return _construct(from: scalar) + return private_construct(from: scalar) } } @@ -343,7 +343,7 @@ extension Int64: ScalarConstructible { /// /// - returns: An instance of `Int64`, if one was successfully extracted from the scalar. public static func construct(from scalar: Node.Scalar) -> Int64? { - return _construct(from: scalar) + return private_construct(from: scalar) } } @@ -356,7 +356,7 @@ extension UInt64: ScalarConstructible { /// /// - returns: An instance of `UInt64`, if one was successfully extracted from the scalar. public static func construct(from scalar: Node.Scalar) -> UInt64? { - return _construct(from: scalar) + return private_construct(from: scalar) } } @@ -420,12 +420,12 @@ extension Dictionary { /// /// - returns: An instance of `[AnyHashable: Any]`, if one was successfully extracted from the mapping. public static func construct_mapping(from mapping: Node.Mapping) -> [AnyHashable: Any]? { - return _construct_mapping(from: mapping) + return private_construct_mapping(from: mapping) } } private extension Dictionary { - static func _construct_mapping(from mapping: Node.Mapping) -> [AnyHashable: Any] { + static func private_construct_mapping(from mapping: Node.Mapping) -> [AnyHashable: Any] { let mapping = mapping.flatten() // TODO: YAML supports keys other than str. return [AnyHashable: Any]( diff --git a/Sources/Yams/Decoder.swift b/Sources/Yams/Decoder.swift index ffd294b8..859c4583 100644 --- a/Sources/Yams/Decoder.swift +++ b/Sources/Yams/Decoder.swift @@ -51,7 +51,7 @@ public class YAMLDecoder { let parser = try Parser(yaml: yaml, resolver: Resolver([.merge]), encoding: 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 + // ^ so we hold an explicit reference to the parser during decoding let node = try parser.singleRoot() ?? "" // ^ nodes only have weak references to Anchors (the Anchors would disappear if not held by the parser) return try self.decode(type, from: node, userInfo: userInfo) @@ -151,24 +151,24 @@ private struct _KeyedDecodingContainer: KeyedDecodingContainerPr init(decoder: _Decoder, wrapping mapping: Node.Mapping) { self.decoder = decoder - + let keys = mapping.keys - + let decodeAnchor: Anchor? let decodeTag: Tag? - + if let anchor = mapping.anchor, keys.contains(.anchorKeyNode) == false { decodeAnchor = anchor } else { decodeAnchor = nil } - + if mapping.tag.name != .implicit && keys.contains(.tagKeyNode) == false { decodeTag = mapping.tag } else { decodeTag = nil } - + switch (decodeAnchor, decodeTag) { case (nil, nil): self.mapping = mapping @@ -426,3 +426,5 @@ extension YAMLDecoder: TopLevelDecoder { } } #endif + +// swiftlint:disable:this file_length diff --git a/Sources/Yams/Emitter.swift b/Sources/Yams/Emitter.swift index 565e1162..295a9d40 100644 --- a/Sources/Yams/Emitter.swift +++ b/Sources/Yams/Emitter.swift @@ -271,10 +271,10 @@ public final class Emitter { /// Set the style for scalars that include newlines public var newLineScalarStyle: Node.Scalar.Style = .any - + /// Redundancy aliasing strategy to use when encoding. Defaults to nil public var redundancyAliasingStrategy: RedundancyAliasingStrategy? - + /// Create `Emitter.Options` with the specified values. /// /// - parameter canonical: Set if the output should be in the "canonical" format described in the YAML @@ -290,7 +290,8 @@ public final class Emitter { /// - parameter sequenceStyle: Set the style for sequences (arrays / lists) /// - parameter mappingStyle: Set the style for mappings (dictionaries) /// - parameter newLineScalarStyle: Set the style for newline-containing scalars - /// - parameter redundancyAliasingStrategy: Set the strategy for identifying redundant structures and automatically aliasing them + /// - parameter redundancyAliasingStrategy: Set the strategy for identifying + /// redundant structures and automatically aliasing them public init(canonical: Bool = false, indent: Int = 0, width: Int = 0, allowUnicode: Bool = false, lineBreak: Emitter.LineBreak = .ln, explicitStart: Bool = false, @@ -338,7 +339,8 @@ public final class Emitter { /// - parameter sequenceStyle: Set the style for sequences (arrays / lists) /// - parameter mappingStyle: Set the style for mappings (dictionaries) /// - parameter newLineScalarStyle: Set the style for newline-containing scalars - /// - parameter redundancyAliasingStrategy: Set the strategy for identifying redundant structures and automatically aliasing them + /// - parameter redundancyAliasingStrategy: Set the strategy for identifying redundant + /// structures and automatically aliasing them public init(canonical: Bool = false, indent: Int = 0, width: Int = 0, @@ -466,12 +468,6 @@ public final class Emitter { } } -//// MARK: - Options Initializer - -extension Emitter.Options { - -} - // MARK: Implementation Details extension Emitter { @@ -489,7 +485,7 @@ extension Emitter { case .alias(let alias): try serializeAlias(alias) } } - + private func serializeAlias(_ alias: Node.Alias) throws { var event = yaml_event_t() let anchor = alias.anchor.rawValue diff --git a/Sources/Yams/Encoder.swift b/Sources/Yams/Encoder.swift index 39b58ac3..b648ba09 100644 --- a/Sources/Yams/Encoder.swift +++ b/Sources/Yams/Encoder.swift @@ -242,7 +242,7 @@ extension _Encoder: SingleValueEncodingContainer { assertCanEncodeNewValue() try encode(yamlEncodable: value) } - + private func encode(yamlEncodable encodable: YAMLEncodable) throws { func encodeNode() { node = encodable.box() @@ -270,7 +270,8 @@ extension _Encoder: SingleValueEncodingContainer { if let encodable = value as? YAMLEncodable { try encode(yamlEncodable: encodable) } else { - if let redundancyAliasingStrategy = userInfo[.redundancyAliasingStrategyKey] as? RedundancyAliasingStrategy { + if let redundancyAliasingStrategy = + userInfo[.redundancyAliasingStrategyKey] as? RedundancyAliasingStrategy { switch try redundancyAliasingStrategy.alias(for: value) { case .none: try value.encode(to: self) diff --git a/Sources/Yams/Node.Alias.swift b/Sources/Yams/Node.Alias.swift index 1c3585bb..80766eff 100644 --- a/Sources/Yams/Node.Alias.swift +++ b/Sources/Yams/Node.Alias.swift @@ -6,8 +6,6 @@ // Copyright (c) 2024 Yams. All rights reserved. // -import Foundation - // MARK: Node+Alias extension Node { @@ -19,7 +17,7 @@ extension Node { public var tag: Tag /// The location for this node. public var mark: Mark? - + /// Create a `Node.Alias` using the specified parameters. /// /// - parameter tag: This scalar's `Tag`. diff --git a/Sources/Yams/Node.Mapping.swift b/Sources/Yams/Node.Mapping.swift index e9635fb8..955939b6 100644 --- a/Sources/Yams/Node.Mapping.swift +++ b/Sources/Yams/Node.Mapping.swift @@ -35,7 +35,11 @@ extension Node { /// - parameter tag: This mapping's `Tag`. /// - parameter style: The style to use when emitting this `Mapping`. /// - parameter mark: This mapping's `Mark`. - public init(_ pairs: [(Node, Node)], _ tag: Tag = .implicit, _ style: Style = .any, _ mark: Mark? = nil, _ anchor: Anchor? = nil) { + public init(_ pairs: [(Node, Node)], + _ tag: Tag = .implicit, + _ style: Style = .any, + _ mark: Mark? = nil, + _ anchor: Anchor? = nil) { self.pairs = pairs.map { Pair($0.0, $0.1) } self.tag = tag self.style = style diff --git a/Sources/Yams/Node.Scalar.swift b/Sources/Yams/Node.Scalar.swift index 68640f0a..42ad2037 100644 --- a/Sources/Yams/Node.Scalar.swift +++ b/Sources/Yams/Node.Scalar.swift @@ -50,7 +50,11 @@ extension Node { /// - parameter tag: This scalar's `Tag`. /// - parameter style: The style to use when emitting this `Scalar`. /// - parameter mark: This scalar's `Mark`. - public init(_ string: String, _ tag: Tag = .implicit, _ style: Style = .any, _ mark: Mark? = nil, _ anchor: Anchor? = nil) { + public init(_ string: String, + _ tag: Tag = .implicit, + _ style: Style = .any, + _ mark: Mark? = nil, + _ anchor: Anchor? = nil) { self.string = string self.tag = tag self.style = style diff --git a/Sources/Yams/Node.Sequence.swift b/Sources/Yams/Node.Sequence.swift index c74f039e..80f7a40f 100644 --- a/Sources/Yams/Node.Sequence.swift +++ b/Sources/Yams/Node.Sequence.swift @@ -37,7 +37,11 @@ extension Node { /// - parameter tag: This sequence's `Tag`. /// - parameter style: The style to use when emitting this `Sequence`. /// - parameter mark: This sequence's `Mark`. - public init(_ nodes: [Node], _ tag: Tag = .implicit, _ style: Style = .any, _ mark: Mark? = nil, _ anchor: Anchor? = nil) { + public init(_ nodes: [Node], + _ tag: Tag = .implicit, + _ style: Style = .any, + _ mark: Mark? = nil, + _ anchor: Anchor? = nil) { self.nodes = nodes self.tag = tag self.style = style diff --git a/Sources/Yams/Node.swift b/Sources/Yams/Node.swift index c848da43..c335ee79 100644 --- a/Sources/Yams/Node.swift +++ b/Sources/Yams/Node.swift @@ -26,7 +26,10 @@ extension Node { /// - parameter string: String value for this node. /// - parameter tag: Tag for this node. /// - parameter style: Style to use when emitting this node. - public init(_ string: String, _ tag: Tag = .implicit, _ style: Scalar.Style = .any, _ anchor: Anchor? = nil) { + public init(_ string: String, + _ tag: Tag = .implicit, + _ style: Scalar.Style = .any, + _ anchor: Anchor? = nil) { self = .scalar(.init(string, tag, style, nil, anchor)) } @@ -35,7 +38,10 @@ extension Node { /// - parameter pairs: Pairs of nodes to use for this node. /// - parameter tag: Tag for this node. /// - parameter style: Style to use when emitting this node. - public init(_ pairs: [(Node, Node)], _ tag: Tag = .implicit, _ style: Mapping.Style = .any, _ anchor: Anchor? = nil) { + public init(_ pairs: [(Node, Node)], + _ tag: Tag = .implicit, + _ style: Mapping.Style = .any, + _ anchor: Anchor? = nil) { self = .mapping(.init(pairs, tag, style, nil, anchor)) } @@ -44,7 +50,10 @@ extension Node { /// - parameter nodes: Sequence of nodes to use for this node. /// - parameter tag: Tag for this node. /// - parameter style: Style to use when emitting this node. - public init(_ nodes: [Node], _ tag: Tag = .implicit, _ style: Sequence.Style = .any, _ anchor: Anchor? = nil) { + public init(_ nodes: [Node], + _ tag: Tag = .implicit, + _ style: Sequence.Style = .any, + _ anchor: Anchor? = nil) { self = .sequence(.init(nodes, tag, style, nil, anchor)) } } @@ -73,7 +82,7 @@ extension Node { case let .alias(alias): return alias.mark } } - + /// The anchor for this node. public var anchor: Anchor? { switch self { @@ -304,7 +313,7 @@ extension Node { } return false } - + func setting(anchor: Anchor) -> Self { switch self { case var .mapping(mapping): @@ -321,7 +330,7 @@ extension Node { return .alias(alias) } } - + func setting(tag: Tag) -> Self { switch self { case var .mapping(mapping): diff --git a/Sources/Yams/Parser.swift b/Sources/Yams/Parser.swift index 948c03be..46165ace 100644 --- a/Sources/Yams/Parser.swift +++ b/Sources/Yams/Parser.swift @@ -265,7 +265,7 @@ public final class Parser { case utf16(Data) } private var buffer: Buffer - + // MARK: – Pivate Mutators private func register(anchor: Anchor?, to node: Node) { if let anchor { @@ -341,7 +341,11 @@ private extension Parser { func loadScalar(from event: Event) throws -> Node { let anchor = event.scalarAnchor - let node = Node.scalar(.init(event.scalarValue, tag(event.scalarTag), event.scalarStyle, event.startMark, anchor)) + let node = Node.scalar(.init(event.scalarValue, + tag(event.scalarTag), + event.scalarStyle, + event.startMark, + anchor)) register(anchor: anchor, to: node) return node } @@ -354,7 +358,11 @@ private extension Parser { event = try parse() } let anchor = firstEvent.sequenceAnchor - let node = Node.sequence(.init(array, tag(firstEvent.sequenceTag), event.sequenceStyle, firstEvent.startMark, anchor)) + let node = Node.sequence(.init(array, + tag(firstEvent.sequenceTag), + event.sequenceStyle, + firstEvent.startMark, + anchor)) register(anchor: anchor, to: node) return node } @@ -370,7 +378,11 @@ private extension Parser { event = try parse() } let anchor = firstEvent.mappingAnchor - let node = Node.mapping(.init(pairs, tag(firstEvent.mappingTag), event.mappingStyle, firstEvent.startMark, anchor)) + let node = Node.mapping(.init(pairs, + tag(firstEvent.mappingTag), + event.mappingStyle, + firstEvent.startMark, + anchor)) register(anchor: anchor, to: node) return node } diff --git a/Sources/Yams/RedundancyAliasingStrategy.swift b/Sources/Yams/RedundancyAliasingStrategy.swift index d802847e..5d655230 100644 --- a/Sources/Yams/RedundancyAliasingStrategy.swift +++ b/Sources/Yams/RedundancyAliasingStrategy.swift @@ -6,8 +6,6 @@ // Copyright (c) 2024 Yams. All rights reserved. // -import Foundation - public enum RedundancyAliasingOutcome { case anchor(Anchor) case alias(Anchor) @@ -24,14 +22,14 @@ public enum RedundancyAliasingOutcome { /// when releaseAnchorReferences() is called by the Encoder. After this call the implementation will no longer be /// referenced by the Encoder and will itself be released. public protocol RedundancyAliasingStrategy: AnyObject { - + /// Implementations should return RedundancyAliasingOutcome.anchor(...) for the first occurrence of a value. /// Subsequent occurrences of the same value (where same-ness is defined by the implementation) should /// return RedundancyAliasingOutcome.alias(...) where the contained Anchor has the same value as the previously /// returned RedundancyAliasingOutcome.anchor(...). Its the identity of the Anchor values returned that ultimately /// informs the YAML encoder when to use aliases. func alias(for encodable: any Encodable) throws -> RedundancyAliasingOutcome - + /// It is essential that implementations release all references to Anchors which are created by this type /// when releaseAnchorReferences() is called by the Encoder. After this call, the implementation will no longer be /// referenced by the Encoder and will itself be released. @@ -43,18 +41,18 @@ public protocol RedundancyAliasingStrategy: AnyObject { /// i.e. if two values are Hashable-Equal, they will be aliased in the resultant YML document. public class HashableAliasingStrategy: RedundancyAliasingStrategy { private var hashesToAliases: [AnyHashable: Anchor] = [:] - + let uniqueAliasProvider = UniqueAliasProvider() - + public init() {} - + public func alias(for encodable: any Encodable) throws -> RedundancyAliasingOutcome { guard let hashable = encodable as? any Hashable & Encodable else { return .none } return try alias(for: hashable) } - + private func alias(for hashable: any Hashable & Encodable) throws -> RedundancyAliasingOutcome { let anyHashable = AnyHashable(hashable) if let existing = hashesToAliases[anyHashable] { @@ -65,23 +63,24 @@ public class HashableAliasingStrategy: RedundancyAliasingStrategy { return .anchor(newAlias) } } - + public func releaseAnchorReferences() throws { hashesToAliases.removeAll() } } -/// An implementation of RedundancyAliasingStrategy that defines alias-ability by the coded representation of the values. -/// i.e. if two values encode to exactly the same, they will be aliased in the resultant YML document even if the values themselves are of different types +/// An implementation of RedundancyAliasingStrategy that defines alias-ability by the coded representation +/// of the values. i.e. if two values encode to exactly the same, they will be aliased in the resultant YML +/// document even if the values themselves are of different types public class StrictEncodableAliasingStrategy: RedundancyAliasingStrategy { private var codedToAliases: [String: Anchor] = [:] - + let uniqueAliasProvider = UniqueAliasProvider() - + public init() {} - + private let encoder = YAMLEncoder() - + public func alias(for encodable: any Encodable) throws -> RedundancyAliasingOutcome { let coded = try encoder.encode(encodable) if let existing = codedToAliases[coded] { @@ -92,7 +91,7 @@ public class StrictEncodableAliasingStrategy: RedundancyAliasingStrategy { return .anchor(newAlias) } } - + public func releaseAnchorReferences() throws { codedToAliases.removeAll() } @@ -100,14 +99,14 @@ public class StrictEncodableAliasingStrategy: RedundancyAliasingStrategy { class UniqueAliasProvider { private var counter = 0 - + func uniqueAlias(for encodable: any Encodable) -> Anchor { if let anchorProviding = encodable as? YamlAnchorProviding, let anchor = anchorProviding.yamlAnchor { return anchor } else { counter += 1 - return Anchor(rawValue: String(counter)) + return Anchor(rawValue: String(counter)) } } } diff --git a/Sources/Yams/YamlAnchorProviding.swift b/Sources/Yams/YamlAnchorProviding.swift index dfc4e899..01eff6a7 100644 --- a/Sources/Yams/YamlAnchorProviding.swift +++ b/Sources/Yams/YamlAnchorProviding.swift @@ -6,8 +6,6 @@ // Copyright (c) 2024 Yams. All rights reserved. // -import Foundation - /// Types that conform to YamlAnchorProviding and Encodable can optionally dictate the name of /// a yaml anchor when they are encoded with YAMLEncoder public protocol YamlAnchorProviding { @@ -26,19 +24,19 @@ internal extension Node { } private final class YamlAnchorFunctionNameProvider: YamlAnchorProviding { - + fileprivate var functionName: StaticString? - + var yamlAnchor: Anchor? { functionName = #function return nil } - + func getName() -> StaticString { _ = yamlAnchor return functionName! } - + func getName() -> String { String(describing: getName() as StaticString) } diff --git a/Sources/Yams/YamlTagProviding.swift b/Sources/Yams/YamlTagProviding.swift index 0f2a2d63..b0beb945 100644 --- a/Sources/Yams/YamlTagProviding.swift +++ b/Sources/Yams/YamlTagProviding.swift @@ -24,19 +24,19 @@ internal extension Node { } private final class YamlTagFunctionNameProvider: YamlTagProviding { - + fileprivate var functionName: StaticString? - + var yamlTag: Tag? { functionName = #function return nil } - + func getName() -> StaticString { _ = yamlTag return functionName! } - + func getName() -> String { String(describing: getName() as StaticString) } diff --git a/Tests/YamsTests/AnchorCodingTests.swift b/Tests/YamsTests/AnchorCodingTests.swift index 24d89cba..997ced71 100644 --- a/Tests/YamsTests/AnchorCodingTests.swift +++ b/Tests/YamsTests/AnchorCodingTests.swift @@ -10,38 +10,42 @@ import XCTest import Yams class AnchorCodingTests: XCTestCase { - + /// Test the encoding of a yaml anchor using a type that conforms to YamlAnchorProviding func testYamlAnchorProviding_valuePresent() throws { - let simpleStruct = SimpleWithAnchor(nested: .init(stringValue: "it's a value"), intValue: 52) - + let simpleStruct = SimpleWithAnchor(nested: + .init(stringValue: "it's a value"), + intValue: 52) + _testRoundTrip(of: simpleStruct, - expectedYAML:""" - &simple - nested: - stringValue: it's a value - intValue: 52 - - """ ) // ^ the Yams.Anchor is encoded as a yaml anchor + expectedYAML: """ + &simple + nested: + stringValue: it's a value + intValue: 52 + + """ ) // ^ the Yams.Anchor is encoded as a yaml anchor } - - /// Test the encoding of a a type that does not conform to YamlAnchorProviding but none the less declares a coding member with the same name + + /// Test the encoding of a a type that does not conform to YamlAnchorProviding but none the less + /// declares a coding member with the same name func testStringTypeAnchorName_valuePresent() throws { - let simpleStruct = SimpleWithStringTypeAnchorName(nested: .init(stringValue: "it's a value"), + let simpleStruct = SimpleWithStringTypeAnchorName(nested: .init(stringValue: "it's a value"), intValue: 52, yamlAnchor: "but typed as a string") - + _testRoundTrip(of: simpleStruct, - expectedYAML:""" - nested: - stringValue: it's a value - intValue: 52 - yamlAnchor: but typed as a string - - """ ) // ^ the member is _not_ treated as an anchor + expectedYAML: """ + nested: + stringValue: it's a value + intValue: 52 + yamlAnchor: but typed as a string + + """ ) // ^ the member is _not_ treated as an anchor } - - /// Nothing interesting happens when a type does not conform to YamlAnchorProviding none the less declares a coding member with the same name but that value is nil + + /// Nothing interesting happens when a type does not conform to YamlAnchorProviding none the less + /// declares a coding member with the same name but that value is nil func testStringTypeAnchorName_valueNotPresent() throws { let expectedStruct = SimpleWithStringTypeAnchorName(nested: .init(stringValue: "it's a value"), intValue: 52, @@ -51,15 +55,18 @@ class AnchorCodingTests: XCTestCase { nested: stringValue: it's a value intValue: 52 - + """) } - + /// This test documents some undesirable behavior, but in an unlikely circumstance. - /// If the decoded type does not conform to YamlAnchorProviding it can still have a coding key called `yamlAnchor` - /// If Yams tries to decode such a type AND the document has a nil value for `yamlAnchor` AND the parent context is a mapping AND that mapping has an actual anchor (in the document) + /// If the decoded type does not conform to YamlAnchorProviding it can still have a coding key called + /// `yamlAnchor` + /// If Yams tries to decode such a type AND the document has a nil value for `yamlAnchor` AND the + /// parent context is a mapping AND that mapping has an actual anchor (in the document) /// THEN Yams wrongly tries to decode the anchor as the declared type of key `yamlAnchor`. - /// If that declared type can be decoded from a scalar string value (like String and RawRepresentable where RawValue == String) then the decoding will actually succeed. + /// If that declared type can be decoded from a scalar string value (like String and RawRepresentable + /// where RawValue == String) then the decoding will actually succeed. /// Which effectively injects an unexpected value into the decoded type. func testStringTypeAnchorName_withAnchorPresent_valueNil() throws { let expectedStruct = SimpleWithStringTypeAnchorName(nested: .init(stringValue: "it's a value"), @@ -71,228 +78,243 @@ class AnchorCodingTests: XCTestCase { nested: stringValue: it's a value intValue: 52 - + """.data(using: decoder.encoding.swiftStringEncoding)! - + let decodedStruct = try decoder.decode(SimpleWithStringTypeAnchorName.self, from: data) - + let fixBulletin = "YESS!!! YOU FIXED IT! See \(#file):\(#line) for explanation." - + // begin assertions of known-but-undesirable behavior XCTAssertNotEqual(decodedStruct, expectedStruct, fixBulletin) // We wish this was equal XCTAssertEqual(decodedStruct.yamlAnchor, "AnActualAnchor", fixBulletin) // we wish .yamlAnchor was nil // end assertions of known-but-undesirable behavior - - + // Check the remainder of the properties that the above confusion did not involve XCTAssertEqual(decodedStruct.nested, expectedStruct.nested) XCTAssertEqual(decodedStruct.intValue, expectedStruct.intValue) } } - + class AnchorAliasingTests: XCTestCase { - + /// CYaml library does not detect identical values and automatically alias them. func testCyamlDoesNotAutoAlias_noAnchor() throws { let simpleNoAnchor = SimpleWithoutAnchor(nested: .init(stringValue: "it's a value"), intValue: 52) let differentTypesOneAnchor = SimplePair(first: simpleNoAnchor, second: simpleNoAnchor) - + _testRoundTrip(of: differentTypesOneAnchor, - expectedYAML:""" - first: - nested: - stringValue: it's a value - intValue: 52 - second: - nested: - stringValue: it's a value - intValue: 52 - - """ ) + expectedYAML: """ + first: + nested: + stringValue: it's a value + intValue: 52 + second: + nested: + stringValue: it's a value + intValue: 52 + + """ ) } - - /// CYaml library does not detect identical values and automatically alias them even if the first occurrence has an anchor. + + /// CYaml library does not detect identical values and automatically alias them even if the first + /// occurrence has an anchor. func testCyamlDoesNotAutoAlias_uniqueAnchor() throws { let simpleStruct = SimpleWithAnchor(nested: .init(stringValue: "it's a value"), intValue: 52) let simpleNoAnchor = SimpleWithoutAnchor(nested: .init(stringValue: "it's a value"), intValue: 52) let differentTypesOneAnchor = SimplePair(first: simpleStruct, second: simpleNoAnchor) - + _testRoundTrip(of: differentTypesOneAnchor, - expectedYAML:""" - first: &simple - nested: - stringValue: it's a value - intValue: 52 - second: - nested: - stringValue: it's a value - intValue: 52 - - """ ) + expectedYAML: """ + first: &simple + nested: + stringValue: it's a value + intValue: 52 + second: + nested: + stringValue: it's a value + intValue: 52 + + """ ) } - + /// CYaml library does not detect identical values and automatically alias them even if they have identical anchors. - // This one is not a shortcoming of CYaml. The yaml spec requires that nodes can shadow earlier anchors. + /// This one is not a shortcoming of CYaml. The yaml spec requires that nodes can shadow earlier anchors. func testCyamlDoesNotAutoAlias_duplicateAnchor() throws { let simpleStruct = SimpleWithAnchor(nested: .init(stringValue: "it's a value"), intValue: 52) let duplicatedStructPair = SimplePair(first: simpleStruct, second: simpleStruct) - + _testRoundTrip(of: duplicatedStructPair, - expectedYAML:""" - first: &simple - nested: - stringValue: it's a value - intValue: 52 - second: &simple - nested: - stringValue: it's a value - intValue: 52 - - """ ) + expectedYAML: """ + first: &simple + nested: + stringValue: it's a value + intValue: 52 + second: &simple + nested: + stringValue: it's a value + intValue: 52 + + """ ) } - - + /// 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 options = YAMLEncoder.Options(redundancyAliasingStrategy: HashableAliasingStrategy()) _testRoundTrip(of: duplicatedStructArray, with: options, - expectedYAML:""" - - &simple - nested: - stringValue: it's a value - intValue: 52 - - *simple - - """ ) + expectedYAML: """ + - &simple + nested: + stringValue: it's a value + intValue: 52 + - *simple + + """ ) } - + /// 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 options = YAMLEncoder.Options(redundancyAliasingStrategy: HashableAliasingStrategy()) _testRoundTrip(of: duplicatedStructArray, with: options, - expectedYAML:""" - - &2 - nested: - stringValue: it's a value - intValue: 52 - - *2 - - """ ) + expectedYAML: """ + - &2 + nested: + stringValue: it's a value + intValue: 52 + - *2 + + """ ) } - - /// 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. + + /// 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 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 options = YAMLEncoder.Options(redundancyAliasingStrategy: HashableAliasingStrategy()) _testRoundTrip(of: differentTypesOneAnchors, with: options, - expectedYAML:""" - first: &simple - nested: &2 - stringValue: it's a value - intValue: &4 52 - second: - nested: *2 - intValue: *4 - - """ ) + expectedYAML: """ + first: &simple + nested: &2 + stringValue: it's a value + intValue: &4 52 + second: + nested: *2 + intValue: *4 + + """ ) } - - /// 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 + + /// 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 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 options = YAMLEncoder.Options(redundancyAliasingStrategy: HashableAliasingStrategy()) _testRoundTrip(of: differentTypesNoAnchors, with: options, - expectedYAML:""" - first: - nested: &3 - stringValue: it's a value - intValue: &5 52 - second: - nested: *3 - intValue: *5 - - """ ) + expectedYAML: """ + first: + nested: &3 + stringValue: it's a value + intValue: &5 52 + second: + nested: *3 + intValue: *5 + + """ ) } - - /// 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. + + /// 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 differentTypesNoAnchors = SimplePair(first: + SimpleWithoutAnchor2(nested: .init(stringValue: "it's a value"), + intValue: 52), + second: + SimpleWithoutAnchor(nested: .init(stringValue: "it's a value"), + intValue: 52)) + var options = YAMLEncoder.Options() options.redundancyAliasingStrategy = StrictEncodableAliasingStrategy() _testRoundTrip(of: differentTypesNoAnchors, with: options, - expectedYAML:""" - first: &2 - nested: - stringValue: it's a value - intValue: 52 - second: *2 - - """ ) + expectedYAML: """ + first: &2 + nested: + stringValue: it's a value + intValue: 52 + second: *2 + + """ ) } - + /// 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. -fileprivate struct NestedStruct: Codable, Hashable { +private struct NestedStruct: Codable, Hashable { let stringValue: String } -fileprivate protocol SimpleProtocol: Codable, Hashable { +private protocol SimpleProtocol: Codable, Hashable { + // swiftlint:disable unused_declaration var nested: NestedStruct { get } + // swiftlint:disable unused_declaration var intValue: Int { get } } -fileprivate struct SimpleWithAnchor: SimpleProtocol, YamlAnchorProviding { +private struct SimpleWithAnchor: SimpleProtocol, YamlAnchorProviding { let nested: NestedStruct let intValue: Int var yamlAnchor: Anchor? = "simple" } -fileprivate struct SimpleWithoutAnchor: SimpleProtocol { +private struct SimpleWithoutAnchor: SimpleProtocol { let nested: NestedStruct let intValue: Int } -fileprivate struct SimpleWithoutAnchor2: SimpleProtocol { +private struct SimpleWithoutAnchor2: SimpleProtocol { let nested: NestedStruct let intValue: Int + // swiftlint:disable unused_declaration var unrelatedValue: String? } -fileprivate struct SimpleWithStringTypeAnchorName: SimpleProtocol { +private struct SimpleWithStringTypeAnchorName: SimpleProtocol { let nested: NestedStruct let intValue: Int var yamlAnchor: String? = "StringTypeAnchor" } - - - diff --git a/Tests/YamsTests/AnchorTolerancesTests.swift b/Tests/YamsTests/AnchorTolerancesTests.swift index 60d126cb..4886a959 100644 --- a/Tests/YamsTests/AnchorTolerancesTests.swift +++ b/Tests/YamsTests/AnchorTolerancesTests.swift @@ -10,70 +10,70 @@ import XCTest import Yams class AnchorTolerancesTests: XCTestCase { - + struct Example: Codable, Hashable { var myCustomAnchorDeclaration: Anchor var extraneousValue: Int } - - /// Any type that is Encodable and contains an `Anchor`value but with a coding key different from YamlAnchorProviding - /// will not encode to a yaml anchor + + /// Any type that is Encodable and contains an `Anchor`value but with a coding key different from + /// YamlAnchorProviding will not encode to a yaml anchor /// This may be unexpected func testAnchorEncoding_undeclaredBehavior() throws { let expectedYAML = """ myCustomAnchorDeclaration: I-did-it-myyyyy-way extraneousValue: 3 - + """ - - let value = Example(myCustomAnchorDeclaration: "I-did-it-myyyyy-way", + + let value = Example(myCustomAnchorDeclaration: "I-did-it-myyyyy-way", extraneousValue: 3) - + let encoder = YAMLEncoder() let producedYAML = try encoder.encode(value) XCTAssertEqual(producedYAML, expectedYAML, "Produced YAML not identical to expected YAML.") } - - /// Any type that is Encodable and contains an `Anchor`value with the same coding key as YamlAnchorProviding - /// will encode to a yaml anchor even though the type does not conform to YamlAnchorProviding - /// This may be unexpected - func testAnchorEncoding_undeclaredBehavior_7() throws { - struct Example: Codable, Hashable { - var yamlAnchor: Anchor - var extraneousValue: Int - } - - let expectedYAML = """ - &I-did-it-myyyyy-way - extraneousValue: 3 - - """ - - let value = Example(yamlAnchor: "I-did-it-myyyyy-way", - extraneousValue: 3) - - let encoder = YAMLEncoder() - let producedYAML = try encoder.encode(value) - XCTAssertEqual(producedYAML, expectedYAML, "Produced YAML not identical to expected YAML.") + + /// Any type that is Encodable and contains an `Anchor`value with the same coding key as + /// YamlAnchorProviding will encode to a yaml anchor even though the type does not conform to + /// YamlAnchorProviding + /// This may be unexpected + func testAnchorEncoding_undeclaredBehavior_7() throws { + struct Example: Codable, Hashable { + var yamlAnchor: Anchor + var extraneousValue: Int } - /// Any type that is Decodable and contains an `Anchor` value but with a coding key different from YamlAnchorProviding - /// will not decode an anchor from the text representation. + let expectedYAML = """ + &I-did-it-myyyyy-way + extraneousValue: 3 + + """ + + let value = Example(yamlAnchor: "I-did-it-myyyyy-way", + extraneousValue: 3) + + let encoder = YAMLEncoder() + let producedYAML = try encoder.encode(value) + XCTAssertEqual(producedYAML, expectedYAML, "Produced YAML not identical to expected YAML.") + } + + /// Any type that is Decodable and contains an `Anchor` value but with a coding key different from + /// YamlAnchorProviding will not decode an anchor from the text representation. /// In this case a key not found error will be thrown during decoding /// This may be unexpected func testAnchorDecoding_undeclaredBehavior_1() throws { let sourceYAML = """ &a-different-tag extraneousValue: 3 - """ let decoder = YAMLDecoder() XCTAssertThrowsError(try decoder.decode(Example.self, from: sourceYAML)) // error is ^^ key not found, "myCustomAnchorDeclaration" } - - /// Any type that is Decodable and contains an `Anchor` value but with a coding key different from YamlAnchorProviding - /// will not decode an anchor from the text representation. + + /// Any type that is Decodable and contains an `Anchor` value but with a coding key different from + /// YamlAnchorProviding will not decode an anchor from the text representation. /// In this case the decoding is successful and the anchor is respected by the parser. /// This may be unexpected func testAnchorDecoding_undeclaredBehavior_6() throws { @@ -84,19 +84,20 @@ class AnchorTolerancesTests: XCTestCase { let sourceYAML = """ &a-different-tag extraneousValue: 3 - + """ - + let expectedValue = Example(myCustomAnchorDeclaration: nil, extraneousValue: 3) - + let decoder = YAMLDecoder() let decodedValue = try decoder.decode(Example.self, from: sourceYAML) XCTAssertEqual(decodedValue, expectedValue, "\(Example.self) did not round-trip to an equal value.") } - - /// Any type that is Decodable and contains an `Anchor` value with the same coding key as YamlAnchorProviding - /// will decode an anchor from the text representation even though the type does not conform to YamlAnchorCoding + + /// Any type that is Decodable and contains an `Anchor` value with the same coding key as + /// YamlAnchorProviding will decode an anchor from the text representation even though the type does + /// not conform to YamlAnchorCoding /// This may be unexpected func testAnchorDecoding_undeclaredBehavior_8() throws { struct Example: Codable, Hashable { @@ -106,19 +107,19 @@ class AnchorTolerancesTests: XCTestCase { let sourceYAML = """ &a-different-tag extraneousValue: 3 - + """ - + let expectedValue = Example(yamlAnchor: "a-different-tag", extraneousValue: 3) - + let decoder = YAMLDecoder() let decodedValue = try decoder.decode(Example.self, from: sourceYAML) XCTAssertEqual(decodedValue, expectedValue, "\(Example.self) did not round-trip to an equal value.") } - - /// Any type that is Decodable and contains an `Anchor` value but with a coding key different from YamlAnchorProviding - /// will not decode an anchor from the text representation. + + /// Any type that is Decodable and contains an `Anchor` value but with a coding key different from + /// YamlAnchorProviding will not decode an anchor from the text representation. /// In this case the decoding is successful and the anchor is respected by the parser. /// This is expected behavior, but in a strange situation. func testAnchorDecoding_undeclaredBehavior_3() throws { @@ -126,37 +127,37 @@ class AnchorTolerancesTests: XCTestCase { &a-different-tag extraneousValue: 3 myCustomAnchorDeclaration: deliver-us-from-evil - + """ let expectedValue = Example(myCustomAnchorDeclaration: "deliver-us-from-evil", extraneousValue: 3) - + let decoder = YAMLDecoder() let decodedValue = try decoder.decode(Example.self, from: sourceYAML) XCTAssertEqual(decodedValue, expectedValue, "\(Example.self) did not round-trip to an equal value.") - + } - - /// Any type that is Decodable and contains an `Anchor` value but with a coding key different from YamlAnchorProviding - /// will not decode an anchor from the text representation. - /// In this case the decoding is successful even though and the `Anchor` was initialized with unsupported characters. - /// The anchor is respected by the parser. + + /// Any type that is Decodable and contains an `Anchor` value but with a coding key different from + /// YamlAnchorProviding will not decode an anchor from the text representation. + /// In this case the decoding is successful even though and the `Anchor` was initialized with + /// unsupported characters. The anchor is respected by the parser. /// This is expected behavior, but in a strange situation. func testAnchorDecoding_undeclaredBehavior_2() throws { let sourceYAML = """ &a-different-tag extraneousValue: 3 myCustomAnchorDeclaration: "deliver us from |()evil" - + """ - + let expectedValue = Example(myCustomAnchorDeclaration: "deliver us from |()evil", extraneousValue: 3) - + let decoder = YAMLDecoder() let decodedValue = try decoder.decode(Example.self, from: sourceYAML) XCTAssertEqual(decodedValue, expectedValue, "\(Example.self) did not round-trip to an equal value.") - + } - + } diff --git a/Tests/YamsTests/EncoderTests.swift b/Tests/YamsTests/EncoderTests.swift index 284cfacf..0d85a3a2 100644 --- a/Tests/YamsTests/EncoderTests.swift +++ b/Tests/YamsTests/EncoderTests.swift @@ -440,10 +440,11 @@ class EncoderTests: XCTestCase { // swiftlint:disable:this type_body_length } internal func _testRoundTrip(of value: T, - with options: YAMLEncoder.Options = .init(), - expectedYAML yamlString: String? = nil, - file: StaticString = #file, - line: UInt = #line) where T: Codable, T: Equatable { + with options: YAMLEncoder.Options = .init(), + expectedYAML yamlString: String? = nil, + file: StaticString = #file, + line: UInt = #line) +where T: Codable, T: Equatable { do { let encoder = YAMLEncoder() encoder.options = options diff --git a/Tests/YamsTests/TagCodingTests.swift b/Tests/YamsTests/TagCodingTests.swift index 1b3e0e6f..6f85828d 100644 --- a/Tests/YamsTests/TagCodingTests.swift +++ b/Tests/YamsTests/TagCodingTests.swift @@ -10,38 +10,40 @@ import XCTest import Yams class TagCodingTests: XCTestCase { - + /// Test the encoding of a yaml tag using a type that conforms to YamlTagProviding func testYamlTagProviding_valuePresent() throws { let simpleStruct = SimpleWithTag(nested: .init(stringValue: "it's a value"), intValue: 52) - + _testRoundTrip(of: simpleStruct, - expectedYAML:""" - ! - nested: - stringValue: it's a value - intValue: 52 - - """ ) // ^ the Yams.Tag is encoded as a yaml tag + expectedYAML: """ + ! + nested: + stringValue: it's a value + intValue: 52 + + """ ) // ^ the Yams.Tag is encoded as a yaml tag } - - /// Test the encoding of a a type that does not conform to YamlTagProviding but none the less declares a coding member with the same name + + /// Test the encoding of a a type that does not conform to YamlTagProviding but none the less declares + /// a coding member with the same name func testStringTypeTagName_valuePresent() throws { let simpleStruct = SimpleWithStringTypeTagName(nested: .init(stringValue: "it's a value"), intValue: 52, yamlTag: "but typed as a string") - + _testRoundTrip(of: simpleStruct, - expectedYAML:""" - nested: - stringValue: it's a value - intValue: 52 - yamlTag: but typed as a string - - """ ) // ^ the member is _not_ treated as an tag + expectedYAML: """ + nested: + stringValue: it's a value + intValue: 52 + yamlTag: but typed as a string + + """ ) // ^ the member is _not_ treated as an tag } - - /// Nothing interesting happens when a type does not conform to YamlTagProviding none the less declares a coding member with the same name but that value is nil + + /// Nothing interesting happens when a type does not conform to YamlTagProviding none the less + /// declares a coding member with the same name but that value is nil func testStringTypeTagName_valueNotPresent() throws { let expectedStruct = SimpleWithStringTypeTagName(nested: .init(stringValue: "it's a value"), intValue: 52, @@ -51,15 +53,18 @@ class TagCodingTests: XCTestCase { nested: stringValue: it's a value intValue: 52 - + """) } - + /// This test documents some undesirable behavior, but in an unlikely circumstance. - /// If the decoded type does not conform to YamlTagProviding it can still have a coding key called `yamlTag` - /// If Yams tries to decode such a type AND the document has a nil value for `yamlTag` AND the parent context is a mapping AND that mapping has an actual tag (in the document) + /// If the decoded type does not conform to YamlTagProviding it can still have a coding key called + /// `yamlTag` + /// If Yams tries to decode such a type AND the document has a nil value for `yamlTag` AND the + /// parent context is a mapping AND that mapping has an actual tag (in the document) /// THEN Yams wrongly tries to decode the tag as the declared type of key `yamlTag`. - /// If that declared type can be decoded from a scalar string value (like String and RawRepresentable where RawValue == String) then the decoding will actually succeed. + /// If that declared type can be decoded from a scalar string value (like String and RawRepresentable + /// where RawValue == String) then the decoding will actually succeed. /// Which effectively injects an unexpected value into the decoded type. func testStringTypeTagName_withTagPresent_valueNil() throws { let expectedStruct = SimpleWithStringTypeTagName(nested: .init(stringValue: "it's a value"), @@ -71,19 +76,18 @@ class TagCodingTests: XCTestCase { nested: stringValue: it's a value intValue: 52 - + """.data(using: decoder.encoding.swiftStringEncoding)! - + let decodedStruct = try decoder.decode(SimpleWithStringTypeTagName.self, from: data) - + let fixBulletin = "YESS!!! YOU FIXED IT! See \(#file):\(#line) for explanation." - + // begin assertions of known-but-undesirable behavior XCTAssertNotEqual(decodedStruct, expectedStruct, fixBulletin) // We wish this was equal XCTAssertEqual(decodedStruct.yamlTag, "An:Actual:Tag", fixBulletin) // we wish .yamlTag was nil // end assertions of known-but-undesirable behavior - - + // Check the remainder of the properties that the above confusion did not involve XCTAssertEqual(decodedStruct.nested, expectedStruct.nested) XCTAssertEqual(decodedStruct.intValue, expectedStruct.intValue) @@ -91,150 +95,166 @@ class TagCodingTests: XCTestCase { } class TagWithAnchorCodingTests: XCTestCase { - + /// If types conform to YamlTagProviding and are Hashable-Equal then HashableAliasingStrategy aliases them func testEncoderAutoAlias_Hashable_duplicateValue_commonTag() throws { let simpleStruct = SimpleWithTag(nested: .init(stringValue: "it's a value"), intValue: 52) let duplicatedStructArray = [simpleStruct, simpleStruct] - + let options = YAMLEncoder.Options(redundancyAliasingStrategy: HashableAliasingStrategy()) _testRoundTrip(of: duplicatedStructArray, with: options, - expectedYAML:""" - - &2 ! - nested: - stringValue: it's a value - intValue: 52 - - *2 - - """ ) + expectedYAML: """ + - &2 ! + nested: + stringValue: it's a value + intValue: 52 + - *2 + + """ ) } - - /// If types conform to YamlTagProviding and are NOT Hashable-Equal then HashableAliasingStrategy does not alias them + + /// If types conform to YamlTagProviding 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_uniqueTag() throws { - let differentTypesOneTags = SimplePair(first: SimpleWithTag(nested: .init(stringValue: "it's a value"), intValue: 52), - second: SimpleWithoutTag(nested: .init(stringValue: "it's a value"), intValue: 52)) - + let differentTypesOneTags = SimplePair(first: + SimpleWithTag(nested: .init(stringValue: "it's a value"), + intValue: 52), + second: + SimpleWithoutTag(nested: .init(stringValue: "it's a value"), + intValue: 52)) + let options = YAMLEncoder.Options(redundancyAliasingStrategy: HashableAliasingStrategy()) _testRoundTrip(of: differentTypesOneTags, with: options, - expectedYAML:""" - first: ! - nested: &3 - stringValue: it's a value - intValue: &5 52 - second: - nested: *3 - intValue: *5 - - """ ) + expectedYAML: """ + first: ! + nested: &3 + stringValue: it's a value + intValue: &5 52 + second: + nested: *3 + intValue: *5 + + """ ) } - - /// If types conform to YamlTagProviding can declare to have the same tag and still be NOT Hashable-Equal then HashableAliasingStrategy does not alias them + + /// If types conform to YamlTagProviding can declare to have the same tag and still be 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_distinctValues_commonTag() throws { - let differentTypesOneTags = SimplePair(first: SimpleWithTag(nested: .init(stringValue: "it's a value"), intValue: 52), - second: SimpleWithTag2(nested: .init(stringValue: "it's a value"), intValue: 52)) - + let differentTypesOneTags = SimplePair(first: + SimpleWithTag(nested: .init(stringValue: "it's a value"), + intValue: 52), + second: + SimpleWithTag2(nested: .init(stringValue: "it's a value"), + intValue: 52)) + let options = YAMLEncoder.Options(redundancyAliasingStrategy: HashableAliasingStrategy()) _testRoundTrip(of: differentTypesOneTags, with: options, - expectedYAML:""" - first: ! - nested: &3 - stringValue: it's a value - intValue: &5 52 - second: ! - nested: *3 - intValue: *5 - - """ ) + expectedYAML: """ + first: ! + nested: &3 + stringValue: it's a value + intValue: &5 52 + second: ! + nested: *3 + intValue: *5 + + """ ) } - - /// If different types conform to YamlTagProviding they can declare to have the same tag and further, have exactly the same encoded representation. - /// In thisi case StrictEncodableAliasingStrategy will still alias them even though they are encoded and decoded from different types. + + /// If different types conform to YamlTagProviding they can declare to have the same tag and further, + /// have exactly the same encoded representation. + /// In thisi case StrictEncodableAliasingStrategy will still alias them even though they are encoded and + /// decoded from different types. func testEncoderAutoAlias_StrictEncodable_distinctValues_commonTag() throws { - let differentTypesOneTags = SimplePair(first: SimpleWithTag(nested: .init(stringValue: "it's a value"), intValue: 52), - second: SimpleWithTag2(nested: .init(stringValue: "it's a value"), intValue: 52)) - + let differentTypesOneTags = SimplePair(first: + SimpleWithTag(nested: .init(stringValue: "it's a value"), + intValue: 52), + second: + SimpleWithTag2(nested: .init(stringValue: "it's a value"), + intValue: 52)) + var options = YAMLEncoder.Options() options.redundancyAliasingStrategy = StrictEncodableAliasingStrategy() _testRoundTrip(of: differentTypesOneTags, with: options, - expectedYAML:""" - first: &2 ! - nested: - stringValue: it's a value - intValue: 52 - second: *2 - - """ ) + expectedYAML: """ + first: &2 ! + nested: + stringValue: it's a value + intValue: 52 + second: *2 + + """ ) } - + /// If types conform to YamlTagProviding and YamlAnchorProviding, both are respected. func testEncoderAutoAlias_Hashable_commonTagAndAnchor() throws { let simpleStruct = SimpleWithTagAndAnchor(nested: .init(stringValue: "it's a value"), intValue: 52) let duplicatedStructArray = [simpleStruct, simpleStruct] - + let options = YAMLEncoder.Options(redundancyAliasingStrategy: HashableAliasingStrategy()) _testRoundTrip(of: duplicatedStructArray, with: options, - expectedYAML:""" - - &simple-Anchor ! - nested: - stringValue: it's a value - intValue: 52 - - *simple-Anchor - - """ ) + expectedYAML: """ + - &simple-Anchor ! + nested: + stringValue: it's a value + intValue: 52 + - *simple-Anchor + + """ ) } - + /// A type used to contain values used during testing private struct SimplePair: Hashable, Codable { let first: First let second: Second } - + } // MARK: - Types used for Tag encoding tests. -fileprivate struct NestedStruct: Codable, Hashable { +private struct NestedStruct: Codable, Hashable { let stringValue: String } -fileprivate protocol SimpleProtocol: Codable, Hashable { +private protocol SimpleProtocol: Codable, Hashable { + // swiftlint:disable unused_declaration var nested: NestedStruct { get } + // swiftlint:disable unused_declaration var intValue: Int { get } } -fileprivate struct SimpleWithTag: SimpleProtocol, YamlTagProviding { +private struct SimpleWithTag: SimpleProtocol, YamlTagProviding { let nested: NestedStruct let intValue: Int var yamlTag: Tag? = "simple" } -fileprivate struct SimpleWithTag2: SimpleProtocol, YamlTagProviding { +private struct SimpleWithTag2: SimpleProtocol, YamlTagProviding { let nested: NestedStruct let intValue: Int var yamlTag: Tag? = "simple" } -fileprivate struct SimpleWithoutTag: SimpleProtocol { +private struct SimpleWithoutTag: SimpleProtocol { let nested: NestedStruct let intValue: Int } -fileprivate struct SimpleWithStringTypeTagName: SimpleProtocol { +private struct SimpleWithStringTypeTagName: SimpleProtocol { let nested: NestedStruct let intValue: Int var yamlTag: String? = "StringTypeTag" } -fileprivate struct SimpleWithTagAndAnchor: SimpleProtocol, YamlTagProviding, YamlAnchorProviding { +private struct SimpleWithTagAndAnchor: SimpleProtocol, YamlTagProviding, YamlAnchorProviding { let nested: NestedStruct let intValue: Int var yamlTag: Tag? = "simple:Tag" var yamlAnchor: Anchor? = "simple-Anchor" } - - diff --git a/Tests/YamsTests/TagTolerancesTests.swift b/Tests/YamsTests/TagTolerancesTests.swift index 62a73e1a..b91109bb 100644 --- a/Tests/YamsTests/TagTolerancesTests.swift +++ b/Tests/YamsTests/TagTolerancesTests.swift @@ -10,32 +10,33 @@ import XCTest import Yams class TagTolerancesTests: XCTestCase { - + struct Example: Codable, Hashable { var myCustomTagDeclaration: Tag var extraneousValue: Int } - - /// Any type that is Encodable and contains an `Tag`value but with a coding key different from YamlTagProviding - /// will not encode to a yaml tag + + /// Any type that is Encodable and contains an `Tag`value but with a coding key different from + /// YamlTagProviding will not encode to a yaml tag /// This may be unexpected func testTagEncoding_undeclaredBehavior() throws { let expectedYAML = """ myCustomTagDeclaration: I-did-it-myyyyy-way extraneousValue: 3 - + """ - - let value = Example(myCustomTagDeclaration: "I-did-it-myyyyy-way", + + let value = Example(myCustomTagDeclaration: "I-did-it-myyyyy-way", extraneousValue: 3) - + let encoder = YAMLEncoder() let producedYAML = try encoder.encode(value) XCTAssertEqual(producedYAML, expectedYAML, "Produced YAML not identical to expected YAML.") } - - /// Any type that is Encodable and contains an `Tag`value with the same coding key as YamlTagProviding - /// will encode to a yaml tag even though the type does not conform to YamlTagProviding + + /// Any type that is Encodable and contains an `Tag`value with the same coding key as + /// YamlTagProviding will encode to a yaml tag even though the type does not conform to + /// YamlTagProviding /// This may be unexpected func testTagEncoding_undeclaredBehavior_7() throws { struct Example: Codable, Hashable { @@ -45,7 +46,7 @@ class TagTolerancesTests: XCTestCase { let expectedYAML = """ ! extraneousValue: 3 - + """ let value = Example(yamlTag: "I-did-it-myyyyy-way", @@ -55,7 +56,7 @@ class TagTolerancesTests: XCTestCase { let producedYAML = try encoder.encode(value) XCTAssertEqual(producedYAML, expectedYAML, "Produced YAML not identical to expected YAML.") } - + /// Tags are oddly permissive, but some characters do get escaped /// This may be unexpected func testTagEncoding_undeclaredBehavior_4() throws { @@ -63,38 +64,38 @@ class TagTolerancesTests: XCTestCase { var yamlTag: Tag? var extraneousValue: Int } - + let expectedYAML = """ ! extraneousValue: 3 - + """ - + let value = Example(yamlTag: "I-did-it-[]-*-|-!-()way", extraneousValue: 3) - + let encoder = YAMLEncoder() let producedYAML = try encoder.encode(value) XCTAssertEqual(producedYAML, expectedYAML, "Produced YAML not identical to expected YAML.") } - /// Any type that is Decodable and contains an `Tag` value but with a coding key different from YamlTagProviding - /// will not decode an tag from the text representation. + /// Any type that is Decodable and contains an `Tag` value but with a coding key different from + /// YamlTagProviding will not decode an tag from the text representation. /// In this case a key not found error will be thrown during decoding /// This may be unexpected func testTagDecoding_undeclaredBehavior_1() throws { let sourceYAML = """ ! extraneousValue: 3 - + """ let decoder = YAMLDecoder() XCTAssertThrowsError(try decoder.decode(Example.self, from: sourceYAML)) // error is ^^ key not found, "myCustomTagDeclaration" } - - /// Any type that is Decodable and contains an `Tag` value but with a coding key different from YamlTagProviding - /// will not decode an tag from the text representation. + + /// Any type that is Decodable and contains an `Tag` value but with a coding key different from + /// YamlTagProviding will not decode an tag from the text representation. /// This may be unexpected func testTagDecoding_undeclaredBehavior_6() throws { struct Example: Codable, Hashable { @@ -104,17 +105,17 @@ class TagTolerancesTests: XCTestCase { let sourceYAML = """ ! extraneousValue: 3 - + """ - + let expectedValue = Example(myCustomTagDeclaration: nil, extraneousValue: 3) - + let decoder = YAMLDecoder() let decodedValue = try decoder.decode(Example.self, from: sourceYAML) XCTAssertEqual(decodedValue, expectedValue, "\(Example.self) did not round-trip to an equal value.") } - + /// Any type that is Decodable and contains an `Tag` value with the same coding key as YamlTagProviding /// will decode an tag from the text representatio even though the type does not conform to YamlTagCoding. /// This may be unexpected @@ -126,17 +127,17 @@ class TagTolerancesTests: XCTestCase { let sourceYAML = """ ! extraneousValue: 3 - + """ - + let expectedValue = Example(yamlTag: "a-different-tag", extraneousValue: 3) - + let decoder = YAMLDecoder() let decodedValue = try decoder.decode(Example.self, from: sourceYAML) XCTAssertEqual(decodedValue, expectedValue, "\(Example.self) did not round-trip to an equal value.") } - + /// Any type that is Decodable and contains an `Tag` value but with a coding key different from YamlTagProviding /// will not decode an tag from the text representation. /// This is expected behavior, but in a strange situation. @@ -145,17 +146,17 @@ class TagTolerancesTests: XCTestCase { ! extraneousValue: 3 myCustomTagDeclaration: deliver-us-from-evil - + """ let expectedValue = Example(myCustomTagDeclaration: "deliver-us-from-evil", extraneousValue: 3) - + let decoder = YAMLDecoder() let decodedValue = try decoder.decode(Example.self, from: sourceYAML) XCTAssertEqual(decodedValue, expectedValue, "\(Example.self) did not round-trip to an equal value.") - + } - + /// Any type that is Decodable and contains an `Tag` value but with a coding key different from YamlTagProviding /// will not decode an tag from the text representation. /// This is expected behavior, but in a strange situation. @@ -164,16 +165,16 @@ class TagTolerancesTests: XCTestCase { ! extraneousValue: 3 myCustomTagDeclaration: "deliver us from |()evil" - + """ - + let expectedValue = Example(myCustomTagDeclaration: "deliver us from |()evil", extraneousValue: 3) - + let decoder = YAMLDecoder() let decodedValue = try decoder.decode(Example.self, from: sourceYAML) XCTAssertEqual(decodedValue, expectedValue, "\(Example.self) did not round-trip to an equal value.") - + } - + } From aab602989bc548c539d1b7817a2b8a268bca5a94 Mon Sep 17 00:00:00 2001 From: Adora Lynch Date: Mon, 21 Oct 2024 16:27:46 -0400 Subject: [PATCH 5/7] Address Jazzy documentation warnings --- Sources/Yams/Anchor.swift | 5 +++++ Sources/Yams/RedundancyAliasingStrategy.swift | 4 ++++ Sources/Yams/YamlAnchorProviding.swift | 2 ++ Sources/Yams/YamlTagProviding.swift | 4 +++- 4 files changed, 14 insertions(+), 1 deletion(-) diff --git a/Sources/Yams/Anchor.swift b/Sources/Yams/Anchor.swift index 30294ad5..4ed859c0 100644 --- a/Sources/Yams/Anchor.swift +++ b/Sources/Yams/Anchor.swift @@ -7,13 +7,17 @@ import Foundation +/// A representation of a YAML tag see: https://yaml.org/spec/1.2.2/ +/// Types interested in Encoding and Decoding Anchors should conform to YamlAnchorProviding and YamlAnchorCoding respectively. public final class Anchor: RawRepresentable, ExpressibleByStringLiteral, Codable, Hashable { + /// A CharacterSet containing only characters which are permitted by the underlying cyaml implementation public static let permittedCharacters = CharacterSet.lowercaseLetters .union(.uppercaseLetters) .union(.decimalDigits) .union(.init(charactersIn: "-_")) + /// Returns true if and only if `string` contains only characters which are also in `permittedCharacters` public static func is_cyamlAlpha(_ string: String) -> Bool { Anchor.permittedCharacters.isSuperset(of: .init(charactersIn: string)) } @@ -29,6 +33,7 @@ public final class Anchor: RawRepresentable, ExpressibleByStringLiteral, Codable } } +/// Conformance of Anchor to CustomStringConvertible returns `rawValue` as `description` extension Anchor: CustomStringConvertible { public var description: String { rawValue } } diff --git a/Sources/Yams/RedundancyAliasingStrategy.swift b/Sources/Yams/RedundancyAliasingStrategy.swift index 5d655230..5e76056c 100644 --- a/Sources/Yams/RedundancyAliasingStrategy.swift +++ b/Sources/Yams/RedundancyAliasingStrategy.swift @@ -6,6 +6,10 @@ // Copyright (c) 2024 Yams. All rights reserved. // +/// An enum indicating the outcome of a `RedundancyAliasingStrategy` +/// if the strategy returns `anchor` the encoder will encode an Anchor +/// if the strategy returns `alias` the encoder will encode an alias to an anchor which should already have been specified. +/// If the strategy returns none the encoder will encode without an anchor or an alias public enum RedundancyAliasingOutcome { case anchor(Anchor) case alias(Anchor) diff --git a/Sources/Yams/YamlAnchorProviding.swift b/Sources/Yams/YamlAnchorProviding.swift index 01eff6a7..fd2f765d 100644 --- a/Sources/Yams/YamlAnchorProviding.swift +++ b/Sources/Yams/YamlAnchorProviding.swift @@ -9,6 +9,7 @@ /// Types that conform to YamlAnchorProviding and Encodable can optionally dictate the name of /// a yaml anchor when they are encoded with YAMLEncoder public protocol YamlAnchorProviding { + /// the Anchor to encode with this node or nil var yamlAnchor: Anchor? { get } } @@ -16,6 +17,7 @@ public protocol YamlAnchorProviding { /// Types that conform to YamlAnchorCoding and Decodable can decode yaml anchors /// from source documents into `Anchor` values for reference or modification in memory. public protocol YamlAnchorCoding: YamlAnchorProviding { + /// the Anchor coded with this node or nil if none is present var yamlAnchor: Anchor? { get set } } diff --git a/Sources/Yams/YamlTagProviding.swift b/Sources/Yams/YamlTagProviding.swift index b0beb945..7b2b0c44 100644 --- a/Sources/Yams/YamlTagProviding.swift +++ b/Sources/Yams/YamlTagProviding.swift @@ -1,6 +1,6 @@ // // YamlTagProviding.swift -// +// Yams // // Created by Adora Lynch on 9/5/24. // Copyright (c) 2024 Yams. All rights reserved. @@ -9,6 +9,7 @@ /// Types that conform to YamlTagProviding and Encodable can optionally dictate the name of /// a yaml tag when they are encoded with YAMLEncoder public protocol YamlTagProviding { + /// the Tag to encode with this node or nil var yamlTag: Tag? { get } } @@ -16,6 +17,7 @@ public protocol YamlTagProviding { /// Types that conform to YamlTagCoding and Decodable can decode yaml tags /// from source documents into `Tag` values for reference or modification in memory. public protocol YamlTagCoding: YamlTagProviding { + /// the Tag coded with this node or nil if none is present var yamlTag: Tag? { get set } } From 0d4734bd85718a33b2fee406a0a97dd35f7a1e30 Mon Sep 17 00:00:00 2001 From: Adora Lynch Date: Mon, 21 Oct 2024 17:59:39 -0400 Subject: [PATCH 6/7] Fix failure in pod lib lint of visionOS and parallelize Cocoapods platforms --- .github/workflows/pod_lib_lint.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pod_lib_lint.yml b/.github/workflows/pod_lib_lint.yml index 205d8c43..a3baf393 100644 --- a/.github/workflows/pod_lib_lint.yml +++ b/.github/workflows/pod_lib_lint.yml @@ -27,7 +27,12 @@ jobs: runs-on: macos-14 env: DEVELOPER_DIR: /Applications/Xcode_15.4.app + strategy: + matrix: + platform: [macOS, iOS, tvOS, visionOS] steps: - uses: actions/checkout@v4 - run: bundle install --path vendor/bundle - - run: bundle exec pod lib lint --verbose + - if: matrix.platform == 'visionOS' + run: xcodebuild -downloadPlatform visionOS + - run: bundle exec pod lib lint --platforms=${{ matrix.platform }} --verbose From af53a9813134c4e445b5f4f4030defb3fd3d418a Mon Sep 17 00:00:00 2001 From: Adora Lynch Date: Fri, 8 Nov 2024 08:35:46 -0500 Subject: [PATCH 7/7] Fix final lint and jazzy errors. --- Sources/Yams/Anchor.swift | 3 ++- Sources/Yams/RedundancyAliasingStrategy.swift | 8 +++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/Sources/Yams/Anchor.swift b/Sources/Yams/Anchor.swift index 4ed859c0..80962aed 100644 --- a/Sources/Yams/Anchor.swift +++ b/Sources/Yams/Anchor.swift @@ -8,7 +8,8 @@ import Foundation /// A representation of a YAML tag see: https://yaml.org/spec/1.2.2/ -/// Types interested in Encoding and Decoding Anchors should conform to YamlAnchorProviding and YamlAnchorCoding respectively. +/// Types interested in Encoding and Decoding Anchors should +/// conform to YamlAnchorProviding and YamlAnchorCoding respectively. public final class Anchor: RawRepresentable, ExpressibleByStringLiteral, Codable, Hashable { /// A CharacterSet containing only characters which are permitted by the underlying cyaml implementation diff --git a/Sources/Yams/RedundancyAliasingStrategy.swift b/Sources/Yams/RedundancyAliasingStrategy.swift index 5e76056c..caf09327 100644 --- a/Sources/Yams/RedundancyAliasingStrategy.swift +++ b/Sources/Yams/RedundancyAliasingStrategy.swift @@ -7,12 +7,12 @@ // /// An enum indicating the outcome of a `RedundancyAliasingStrategy` -/// if the strategy returns `anchor` the encoder will encode an Anchor -/// if the strategy returns `alias` the encoder will encode an alias to an anchor which should already have been specified. -/// If the strategy returns none the encoder will encode without an anchor or an alias public enum RedundancyAliasingOutcome { + /// encoder will encode an Anchor case anchor(Anchor) + /// encoder will encode an alias to an anchor which should already have been specified. case alias(Anchor) + /// encoder will encode without an anchor or an alias case none } @@ -48,6 +48,7 @@ public class HashableAliasingStrategy: RedundancyAliasingStrategy { let uniqueAliasProvider = UniqueAliasProvider() + /// Initialize a new HashableAliasingStrategy public init() {} public func alias(for encodable: any Encodable) throws -> RedundancyAliasingOutcome { @@ -81,6 +82,7 @@ public class StrictEncodableAliasingStrategy: RedundancyAliasingStrategy { let uniqueAliasProvider = UniqueAliasProvider() + /// Initialize a new StrictEncodableAliasingStrategy public init() {} private let encoder = YAMLEncoder()