diff --git a/CHANGELOG.md b/CHANGELOG.md index 5780a0a..3be8ec5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -Nothing yet. +### Fixed + +- Treat more-indented plain scalar continuation lines that start with `-` as + text unless they appear at a known sequence-entry indentation. This fixes + OpenAPI-style `allOf` descriptions while preserving nested block sequences + such as `- - 123`. +- Parse explicit scalar keys whose `:` value starts a same-line mapping and + continues with indented mapping siblings, matching large OpenAPI schema keys. ## [0.1.0] - 2026-06-05 diff --git a/Sources/Parsing/Scanner.Helpers.swift b/Sources/Parsing/Scanner.Helpers.swift index 1767bde..80b6f7a 100644 --- a/Sources/Parsing/Scanner.Helpers.swift +++ b/Sources/Parsing/Scanner.Helpers.swift @@ -138,14 +138,14 @@ extension PureYAML.Parsing.Scanner { guard !probe.isAtEnd, !isLineBreak(probe.peek()), probe.peek() != "#" else { return value } - guard !isBlockEntry(probe) else { - return value - } let baseIndentation = plainScalarContinuationBaseIndentation(state: state) guard indentationWidth > baseIndentation else { return value } + if isBlockEntry(probe), state.validImplicitIndentation.contains(indentationWidth) { + return value + } var continuationState = state continuationState.reader = probe diff --git a/Sources/Parsing/TokenEventParser.Helpers.swift b/Sources/Parsing/TokenEventParser.Helpers.swift index 527c7ab..b20cbac 100644 --- a/Sources/Parsing/TokenEventParser.Helpers.swift +++ b/Sources/Parsing/TokenEventParser.Helpers.swift @@ -30,6 +30,13 @@ extension PureYAML.Parsing.TokenEventParser { if properties == .none, canParsePlainScalarWithContinuation(after: valueIndicator) { return try parsePlainScalarWithContinuation(properties: properties) } + if cursor.current?.kind.isMappingKey == true, hasNodeOnSameLine(after: valueIndicator) { + return try parseBlockMapping( + properties: properties, + includeIndentedContinuation: true, + minimumColumn: cursor.current?.mark.column, + ) + } if properties != .none || hasNodeOnSameLine(after: valueIndicator) { return try parseNode(properties: properties) } diff --git a/Tests/CollectionCompatibilityFixtures.swift b/Tests/CollectionCompatibilityFixtures.swift index 2d1ce51..1c57d19 100644 --- a/Tests/CollectionCompatibilityFixtures.swift +++ b/Tests/CollectionCompatibilityFixtures.swift @@ -166,6 +166,39 @@ enum CollectionCompatibilityFixtures { ]))), ])), ), + SuccessCase( + name: "mapping value with indentless sequence of inline mappings", + yaml: """ + schema: + allOf: + - $ref: '#/components/schemas/ErrorCategory' + - description: An integer value that specifies the category of a query + failure error. Values include 1 - System, 2 - User, and 3 + - Other. + """, + expected: .mapping(.init([ + .init(key: "schema", value: .mapping(.init([ + .init(key: "allOf", value: .sequence([ + .mapping(.init([ + .init( + key: "$ref", + value: .string("#/components/schemas/ErrorCategory"), + ), + ])), + .mapping(.init([ + .init( + key: "description", + value: .string( + "An integer value that specifies the category of a query " + + "failure error. Values include 1 - System, 2 - User, and 3 " + + "- Other.", + ), + ), + ])), + ])), + ]))), + ])), + ), ] static let parseErrors: [ParseErrorCase] = [ diff --git a/Tests/ComplexMappingKeyParsingTests.swift b/Tests/ComplexMappingKeyParsingTests.swift index 628bcde..fe3f43d 100644 --- a/Tests/ComplexMappingKeyParsingTests.swift +++ b/Tests/ComplexMappingKeyParsingTests.swift @@ -84,6 +84,34 @@ struct ComplexMappingKeyParsingTests { #expect(value.rootMapping?["[\"a\", \"b\"]"] == nil) } + @Test("Parses explicit scalar keys with same-line mapping values") + func test_parsesExplicitScalarKeysWithSameLineMappingValues() throws { + let value = try PureYAML.parse(""" + ? __stringMin14PatternS3Captions + : minLength: 14 + pattern: ^((s3://(.*?)\\.(srt|SRT))|(https?://(.*?)\\.(srt|SRT)))$ + type: string + next: done + """) + let key = PureYAML.Model.Key.string("__stringMin14PatternS3Captions") + + #expect(value == .mapping(.init([ + .init(keyNode: key, value: .mapping(.init([ + .init(key: "minLength", value: .int(14)), + .init( + key: "pattern", + value: .string("^((s3://(.*?)\\.(srt|SRT))|(https?://(.*?)\\.(srt|SRT)))$"), + ), + .init(key: "type", value: .string("string")), + ]))), + .init(key: "next", value: .string("done")), + ]))) + #expect(value.rootMapping?[key]?.rootMapping?["minLength"] == .int(14)) + #expect(value.rootMapping?[key]?.rootMapping?["type"] == .string("string")) + #expect(value.rootMapping?["next"] == .string("done")) + #expect(value.rootMapping?["minLength"] == nil) + } + @Test("Expands merged mappings with complex-key local overrides") func test_expandsMergedMappingsWithComplexKeyLocalOverrides() throws { let value = try PureYAML.parse(""" diff --git a/docs/rules/research-compatibility.md b/docs/rules/research-compatibility.md index 259a4eb..9228c0f 100644 --- a/docs/rules/research-compatibility.md +++ b/docs/rules/research-compatibility.md @@ -43,3 +43,24 @@ Every compatibility test must pin at least one of: - exact validation issue; - exact path-aware decoding or encoding error; - exact required absence check for fields that must not appear. + +## Parser Compatibility Lessons + +YAML compatibility is parser-state compatibility, not only value-model +compatibility. Indicator characters such as `-`, `?`, and `:` are meaningful +only after indentation, flow depth, and the current parser production are taken +into account. + +When fixing scanner or parser behavior: + +- preserve tests for the exact production corpus shape that failed, not only a + smaller feature in isolation; +- combine interacting YAML features in one regression fixture when the bug + requires their interaction; +- keep implementation fixes grammar-level and content-independent; never branch + on corpus file names, OpenAPI field names, vendor names, schema prefixes, or + other production-data fingerprints; +- for plain scalar continuation bugs, cover both sides explicitly: + - a more-indented continuation line that starts with an indicator but remains + text; + - a same-context indicator line that must remain a collection item.