diff --git a/CHANGELOG.md b/CHANGELOG.md index 5764545..bf638b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- Merge-key (`<<`) override resolution now lives in a single internal helper, + `Parsing.MergeResolution`, shared by the buffered and streaming event + composers. Both paths previously carried an identical copy of the + inherited-versus-local pair merge, so the subtle precedence rules (local keys + win, earlier sources win, inherited-then-local ordering) now have one source + of truth. No behavior change. + ### Documentation - Correct `SECURITY.md`: fix the reporting contact address and replace the diff --git a/Sources/Parsing/EventComposer.Merge.swift b/Sources/Parsing/EventComposer.Merge.swift index baabc6d..d4cde07 100644 --- a/Sources/Parsing/EventComposer.Merge.swift +++ b/Sources/Parsing/EventComposer.Merge.swift @@ -56,12 +56,12 @@ extension PureYAML.Parsing.EventComposer { case let .sequence(values): return try values.map { value in guard case let .mapping(mapping) = value else { - throw invalidMergeValue(mark: mark) + throw PureYAML.Parsing.MergeResolution.invalidMergeValue(mark: mark) } return mapping.pairs } case .bool, .double, .int, .null, .string: - throw invalidMergeValue(mark: mark) + throw PureYAML.Parsing.MergeResolution.invalidMergeValue(mark: mark) } } @@ -70,42 +70,8 @@ extension PureYAML.Parsing.EventComposer { mark: PureYAML.Parsing.Mark, ) throws -> [PureYAML.Model.Pair] { guard case let .mapping(mapping) = value else { - throw invalidMergeValue(mark: mark) + throw PureYAML.Parsing.MergeResolution.invalidMergeValue(mark: mark) } return mapping.pairs } - - func mergedPairs( - from mergeSources: [[PureYAML.Model.Pair]], - localPairs: [PureYAML.Model.Pair], - ) -> [PureYAML.Model.Pair] { - guard !mergeSources.isEmpty else { - return localPairs - } - - let localKeys = Set(localPairs.map(\.keyNode)) - var inheritedKeys = Set() - var inheritedPairs: [PureYAML.Model.Pair] = [] - for source in mergeSources { - var sourceKeys = Set() - for pair in source { - guard !localKeys.contains(pair.keyNode), - !inheritedKeys.contains(pair.keyNode) - else { - continue - } - inheritedPairs.append(pair) - sourceKeys.insert(pair.keyNode) - } - inheritedKeys.formUnion(sourceKeys) - } - return inheritedPairs + localPairs - } - - func invalidMergeValue(mark: PureYAML.Parsing.Mark) -> PureYAML.Parsing.ParseError { - PureYAML.Parsing.ParseError.invalidMergeValue( - line: mark.line, - column: mark.column, - ) - } } diff --git a/Sources/Parsing/EventComposer.swift b/Sources/Parsing/EventComposer.swift index 24d88f4..bee2666 100644 --- a/Sources/Parsing/EventComposer.swift +++ b/Sources/Parsing/EventComposer.swift @@ -149,7 +149,7 @@ extension PureYAML.Parsing.EventComposer { } } _ = try expect("mapping end") { $0.isMappingEnd } - let pairs = mergedPairs(from: mergeSources, localPairs: localPairs) + let pairs = PureYAML.Parsing.MergeResolution.mergedPairs(from: mergeSources, localPairs: localPairs) return .mapping(PureYAML.Model.Mapping(pairs)) } diff --git a/Sources/Parsing/EventStreamComposer.swift b/Sources/Parsing/EventStreamComposer.swift index 3945ad3..0bc5971 100644 --- a/Sources/Parsing/EventStreamComposer.swift +++ b/Sources/Parsing/EventStreamComposer.swift @@ -182,7 +182,7 @@ private extension PureYAML.Parsing.EventStreamComposer { guard container.pendingKey == nil else { throw unexpectedEvent(expected: "mapping value", mark: mark) } - let pairs = mergedPairs( + let pairs = PureYAML.Parsing.MergeResolution.mergedPairs( from: container.mergeSources, localPairs: container.localPairs, ) @@ -251,7 +251,7 @@ private extension PureYAML.Parsing.EventStreamComposer { guard !node.sequenceItems.isEmpty else { return try values.map { value in guard case let .mapping(mapping) = value else { - throw invalidMergeValue(mark: node.mark) + throw PureYAML.Parsing.MergeResolution.invalidMergeValue(mark: node.mark) } return mapping.pairs } @@ -259,51 +259,15 @@ private extension PureYAML.Parsing.EventStreamComposer { return try values.enumerated().map { index, value in let mark = node.sequenceItems[index].mark guard case let .mapping(mapping) = value else { - throw invalidMergeValue(mark: mark) + throw PureYAML.Parsing.MergeResolution.invalidMergeValue(mark: mark) } return mapping.pairs } case .bool, .double, .int, .null, .string: - throw invalidMergeValue(mark: node.mark) + throw PureYAML.Parsing.MergeResolution.invalidMergeValue(mark: node.mark) } } - func mergedPairs( - from mergeSources: [[PureYAML.Model.Pair]], - localPairs: [PureYAML.Model.Pair], - ) -> [PureYAML.Model.Pair] { - guard !mergeSources.isEmpty else { - return localPairs - } - - let localKeys = Set(localPairs.map(\.keyNode)) - var inheritedKeys = Set() - var inheritedPairs: [PureYAML.Model.Pair] = [] - for source in mergeSources { - var sourceKeys = Set() - for pair in source { - guard !localKeys.contains(pair.keyNode), - !inheritedKeys.contains(pair.keyNode) - else { - continue - } - inheritedPairs.append(pair) - sourceKeys.insert(pair.keyNode) - } - inheritedKeys.formUnion(sourceKeys) - } - return inheritedPairs + localPairs - } - - func invalidMergeValue( - mark: PureYAML.Parsing.Mark, - ) -> PureYAML.Parsing.ParseError { - PureYAML.Parsing.ParseError.invalidMergeValue( - line: mark.line, - column: mark.column, - ) - } - func unexpectedEvent( expected: String, mark: PureYAML.Parsing.Mark, diff --git a/Sources/Parsing/MergeResolution.swift b/Sources/Parsing/MergeResolution.swift new file mode 100644 index 0000000..0648ca3 --- /dev/null +++ b/Sources/Parsing/MergeResolution.swift @@ -0,0 +1,46 @@ +extension PureYAML.Parsing { + /// Shared merge-key (`<<`) resolution used by both the buffered and the + /// streaming event composers, so the override semantics live in one place. + enum MergeResolution { + /// Combine merge sources with local pairs. + /// + /// Local keys win over inherited keys, and earlier merge sources win + /// over later ones. Insertion order is inherited pairs first, then the + /// document's own local pairs. + static func mergedPairs( + from mergeSources: [[PureYAML.Model.Pair]], + localPairs: [PureYAML.Model.Pair], + ) -> [PureYAML.Model.Pair] { + guard !mergeSources.isEmpty else { + return localPairs + } + + let localKeys = Set(localPairs.map(\.keyNode)) + var inheritedKeys = Set() + var inheritedPairs: [PureYAML.Model.Pair] = [] + for source in mergeSources { + var sourceKeys = Set() + for pair in source { + guard !localKeys.contains(pair.keyNode), + !inheritedKeys.contains(pair.keyNode) + else { + continue + } + inheritedPairs.append(pair) + sourceKeys.insert(pair.keyNode) + } + inheritedKeys.formUnion(sourceKeys) + } + return inheritedPairs + localPairs + } + + /// The parse error raised when a merge value is neither a mapping nor a + /// sequence of mappings. + static func invalidMergeValue(mark: PureYAML.Parsing.Mark) -> PureYAML.Parsing.ParseError { + PureYAML.Parsing.ParseError.invalidMergeValue( + line: mark.line, + column: mark.column, + ) + } + } +}