Harden parser against real YAML fixtures

This commit is contained in:
Mihaela Mihaljevic 2026-06-05 19:34:30 +02:00
parent 906f0c454d
commit bc54528353
36 changed files with 262936 additions and 154 deletions

1
.gitignore vendored
View File

@ -72,6 +72,7 @@
# Tooling logs (cron, hooks, local scripts)
*.log
**/*.log
.derived-data-log-*
# Local SQLite DB created by autoMigrate at first boot.
# Codeweaver scaffolds (template-time/bookings/crm/inventory) all default

View File

@ -18,6 +18,7 @@
--wrapparameters before-first
--wrapcollections before-first
--self remove
--exclude .build,DerivedData
# PureYAML uses OpenAPIKit-style `test_...` method names with Swift Testing.
--disable swiftTestingTestCaseNames

View File

@ -7,6 +7,7 @@ opt_in_rules:
- force_unwrapping
excluded:
- .build
- DerivedData
force_unwrapping: error
force_try: error
force_cast: error

View File

@ -30,11 +30,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Add first-class complex mapping keys through `Model.Pair.keyNode`, including
parsing, deterministic validation paths, duplicate-key checks, and block/flow
dumping for sequence and mapping keys.
- Add a real-world YAML fixture corpus with representative assertions for
short, medium, and very large documents, including OpenAPI and Kubernetes
examples.
- Add deliberately failing YAML fixtures that demonstrate exact validation
diagnostics, strict failures, and non-strict warning returns.
### Changed
- Tokenize YAML document start and end markers so scanner, event-parser, and
stream tests can pin exact document-boundary behavior.
- Harden block mapping and multiline plain-scalar parsing around nested
sequences, compact mapping entries, explicit complex keys, tagged keys, and
lower-column mapping siblings.
- Keep quoted `"<<":` and explicitly string-tagged `!!str <<` keys as ordinary
string keys while merge syntax is expanded during parsing.
- Keep Foundation-backed tags such as `!!timestamp` and `!!binary` as ordinary

View File

@ -20,6 +20,9 @@ let package = Package(
name: "PureYAMLTests",
dependencies: ["PureYAML"],
path: "Tests",
resources: [
.copy("Fixtures"),
],
),
],
)

View File

@ -37,6 +37,13 @@ extension PureYAML.Parsing.Scanner {
|| character == "}"
}
func isBlockMappingValueBoundary(_ character: Character?) -> Bool {
guard let character else {
return true
}
return character == " " || isLineBreak(character)
}
func isTokenTerminator(_ character: Character) -> Bool {
character == " "
|| isLineBreak(character)
@ -48,6 +55,29 @@ extension PureYAML.Parsing.Scanner {
|| character == "#"
}
func canStartAnchorName(_ character: Character?) -> Bool {
guard let character else {
return false
}
return !isTokenTerminator(character)
&& character != "*"
&& character != "&"
&& character != "!"
&& character != "\""
&& character != "'"
}
func hasNodeOnSameLine(afterCurrentPositionIn reader: PureYAML.Parsing.Reader) -> Bool {
var offset = 0
while reader.peek(offset: offset) == " " {
offset += 1
}
guard let character = reader.peek(offset: offset) else {
return false
}
return !isLineBreak(character) && character != "#"
}
func isFlowDelimiter(_ character: Character) -> Bool {
character == ","
|| character == "["
@ -74,6 +104,91 @@ extension PureYAML.Parsing.Scanner {
return String(value[..<end])
}
func scanPlainScalarContinuations(
after firstLine: String,
state: inout State,
) -> String {
var value = firstLine
while true {
var probe = state.reader
guard isLineBreak(probe.peek()) else {
return value
}
probe.advance()
var blankLineCount = 0
while true {
var blankProbe = probe
_ = blankProbe.consume { character in
character == " "
}
guard isLineBreak(blankProbe.peek()) else {
break
}
blankProbe.advance()
probe = blankProbe
blankLineCount += 1
}
_ = probe.consume { character in
character == " "
}
let indentationWidth = probe.mark.column - 1
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
}
var continuationState = state
continuationState.reader = probe
let continuation = scanPlainScalarContinuationLine(&continuationState)
guard !isAtMappingValueIndicator(state: continuationState) else {
return value
}
state = continuationState
if blankLineCount == 0 {
value += " "
} else {
value += String(repeating: "\n", count: blankLineCount)
}
value += continuation
}
}
func plainScalarContinuationBaseIndentation(state: State) -> Int {
state.indentation.last ?? 0
}
func scanPlainScalarContinuationLine(_ state: inout State) -> String {
var value = ""
while let character = state.reader.peek(), !isLineBreak(character) {
if character == " ", state.reader.peek(offset: 1) == "#" {
break
}
if character == "#", value.isEmpty || value.last == " " {
break
}
if character == ":", shouldEndPlainScalarAtMappingValue(state: state) {
break
}
if state.flowDepth > 0, isFlowDelimiter(character) {
break
}
value.append(character)
state.reader.advance()
}
return trimTrailingSpaces(value)
}
func skipLine(_ state: inout State) {
_ = state.reader.consume { character in
!isLineBreak(character)
@ -108,7 +223,7 @@ extension PureYAML.Parsing.Scanner {
if character == "#", accumulated.isEmpty || accumulated.last == " " {
return true
}
if character == ":", isMappingValueBoundary(state.reader.peek(offset: 1)) {
if character == ":", shouldEndPlainScalarAtMappingValue(state: state) {
return true
}
if state.flowDepth > 0, isFlowDelimiter(character) {
@ -116,4 +231,15 @@ extension PureYAML.Parsing.Scanner {
}
return false
}
func shouldEndPlainScalarAtMappingValue(state: State) -> Bool {
if state.flowDepth > 0 {
return isMappingValueBoundary(state.reader.peek(offset: 1))
}
return isBlockMappingValueBoundary(state.reader.peek(offset: 1))
}
func isAtMappingValueIndicator(state: State) -> Bool {
state.reader.peek() == ":" && shouldEndPlainScalarAtMappingValue(state: state)
}
}

View File

@ -0,0 +1,153 @@
extension PureYAML.Parsing.Scanner {
func scanIndentation(_ state: inout State) throws {
let start = state.reader.mark
let width = try scanIndentationWidth(&state)
guard shouldProcessIndentation(state) else {
return
}
let end = state.reader.mark
let current = state.indentation.last ?? 0
if consumeBlockScalarContentIndentation(width: width, state: &state) {
return
}
if let indicator = state.pendingBlockScalarIndentationIndicator {
try applyExplicitBlockScalarIndentation(
indicator: indicator,
width: width,
current: current,
marks: (start: start, end: end),
state: &state,
)
} else if width > current {
applyIncreasedIndentation(width: width, start: start, end: end, state: &state)
} else if width < current {
try applyDedentation(width: width, start: start, end: end, state: &state)
}
pruneImplicitIndentation(&state)
clearPendingBlockScalarIndentation(&state)
}
func scanIndentationWidth(_ state: inout State) throws -> Int {
var width = 0
while let character = state.reader.peek() {
if character == " " {
width += 1
state.reader.advance()
} else if character == "\t" {
throw PureYAML.Parsing.ParseError.tabIndentation(line: state.reader.mark.line)
} else {
return width
}
}
return width
}
func shouldProcessIndentation(_ state: State) -> Bool {
guard !state.reader.isAtEnd, !isLineBreak(state.reader.peek()) else {
return false
}
guard state.reader.peek() != "#" else {
return false
}
return state.flowDepth == 0
}
func consumeBlockScalarContentIndentation(
width: Int,
state: inout State,
) -> Bool {
guard let blockScalarIndentation = state.blockScalarIndentation else {
return false
}
guard width < blockScalarIndentation else {
state.blockScalarContentPrefix = String(repeating: " ", count: width - blockScalarIndentation)
return true
}
state.blockScalarIndentation = nil
state.blockScalarContentPrefix = ""
return false
}
func applyExplicitBlockScalarIndentation(
indicator: Int,
width: Int,
current: Int,
marks: (start: PureYAML.Parsing.Mark, end: PureYAML.Parsing.Mark),
state: inout State,
) throws {
let blockScalarIndentation = current + indicator
guard width >= blockScalarIndentation else {
throw PureYAML.Parsing.ParseError.unexpectedIndentation(line: marks.end.line)
}
state.indentation.append(blockScalarIndentation)
state.append(.indent(width: blockScalarIndentation), mark: marks.start, endMark: marks.end)
state.blockScalarIndentation = blockScalarIndentation
state.blockScalarContentPrefix = String(repeating: " ", count: width - blockScalarIndentation)
clearPendingBlockScalarIndentation(&state)
}
func applyIncreasedIndentation(
width: Int,
start: PureYAML.Parsing.Mark,
end: PureYAML.Parsing.Mark,
state: inout State,
) {
state.indentation.append(width)
state.append(.indent(width: width), mark: start, endMark: end)
guard state.pendingBlockScalarIndentation else {
return
}
state.blockScalarIndentation = width
state.blockScalarContentPrefix = ""
clearPendingBlockScalarIndentation(&state)
}
func applyDedentation(
width: Int,
start: PureYAML.Parsing.Mark,
end: PureYAML.Parsing.Mark,
state: inout State,
) throws {
while state.indentation.count > 1, width < (state.indentation.last ?? 0) {
state.indentation.removeLast()
state.append(.dedent(width: state.indentation.last ?? 0), mark: end, endMark: end)
}
if canUseImplicitIndentation(width: width, state: state) {
state.indentation.append(width)
state.append(.indent(width: width), mark: start, endMark: end)
}
guard width == state.indentation.last else {
throw PureYAML.Parsing.ParseError.unexpectedIndentation(line: end.line)
}
}
func canUseImplicitIndentation(
width: Int,
state: State,
) -> Bool {
width != state.indentation.last
&& width > (state.indentation.last ?? 0)
&& state.validImplicitIndentation.contains(width)
}
func pruneImplicitIndentation(_ state: inout State) {
let current = state.indentation.last ?? 0
state.validImplicitIndentation = state.validImplicitIndentation.filter { $0 <= current }
}
func clearPendingBlockScalarIndentation(_ state: inout State) {
state.pendingBlockScalarIndentation = false
state.pendingBlockScalarIndentationIndicator = nil
}
func closeIndentation(_ state: inout State) {
let mark = state.reader.mark
while state.indentation.count > 1 {
state.indentation.removeLast()
state.append(.dedent(width: state.indentation.last ?? 0), mark: mark, endMark: mark)
}
}
}

View File

@ -47,6 +47,11 @@ extension PureYAML.Parsing.Scanner {
var hasDocumentStartMarker = false
var isDocumentClosed = false
var flowDepth = 0
var validImplicitIndentation: Set<Int> = []
var pendingBlockScalarIndentation = false
var pendingBlockScalarIndentationIndicator: Int?
var blockScalarIndentation: Int?
var blockScalarContentPrefix = ""
var tagHandles = [
"!": "!",
"!!": "tag:yaml.org,2002:",
@ -79,60 +84,16 @@ extension PureYAML.Parsing.Scanner {
}
extension PureYAML.Parsing.Scanner {
func scanIndentation(_ state: inout State) throws {
let start = state.reader.mark
var width = 0
while let character = state.reader.peek() {
if character == " " {
width += 1
state.reader.advance()
} else if character == "\t" {
throw PureYAML.Parsing.ParseError.tabIndentation(line: state.reader.mark.line)
} else {
break
}
}
guard !state.reader.isAtEnd, !isLineBreak(state.reader.peek()) else {
return
}
guard state.reader.peek() != "#" else {
return
}
guard state.flowDepth == 0 else {
return
}
let end = state.reader.mark
let current = state.indentation.last ?? 0
if width > current {
state.indentation.append(width)
state.append(.indent(width: width), mark: start, endMark: end)
} else if width < current {
while state.indentation.count > 1, width < (state.indentation.last ?? 0) {
state.indentation.removeLast()
state.append(.dedent(width: state.indentation.last ?? 0), mark: end, endMark: end)
}
guard width == state.indentation.last else {
throw PureYAML.Parsing.ParseError.unexpectedIndentation(line: end.line)
}
}
}
func closeIndentation(_ state: inout State) {
let mark = state.reader.mark
while state.indentation.count > 1 {
state.indentation.removeLast()
state.append(.dedent(width: state.indentation.last ?? 0), mark: mark, endMark: mark)
}
}
func scanToken(_ state: inout State) throws {
skipSeparation(&state)
guard let character = state.reader.peek(), !isLineBreak(character) else {
return
}
if state.blockScalarIndentation != nil {
scanBlockScalarContent(&state)
return
}
if try scanLineIndicatorToken(character, state: &state) {
return
}
@ -261,6 +222,9 @@ extension PureYAML.Parsing.Scanner {
let start = state.reader.mark
state.reader.advance()
state.append(kind, mark: start, endMark: state.reader.mark)
if case .blockEntry = kind, hasNodeOnSameLine(afterCurrentPositionIn: state.reader) {
state.validImplicitIndentation.insert(start.column + 1)
}
}
func scanComment(_ state: inout State) {
@ -316,10 +280,13 @@ extension PureYAML.Parsing.Scanner {
) {
let start = state.reader.mark
var chomping = PureYAML.Parsing.BlockScalarChomping.clip
var indentationIndicator: Int?
state.reader.advance()
while let character = state.reader.peek(), isBlockScalarHeaderCharacter(character) {
if character == "-" {
chomping = .strip
} else if character.isNumber {
indentationIndicator = Int(String(character))
}
state.reader.advance()
}
@ -328,6 +295,17 @@ extension PureYAML.Parsing.Scanner {
mark: start,
endMark: state.reader.mark,
)
state.pendingBlockScalarIndentation = true
state.pendingBlockScalarIndentationIndicator = indentationIndicator
}
func scanBlockScalarContent(_ state: inout State) {
let start = state.reader.mark
let value = state.blockScalarContentPrefix + state.reader.consume { character in
!isLineBreak(character)
}
state.blockScalarContentPrefix = ""
state.append(.scalar(value: value, style: .plain), mark: start, endMark: state.reader.mark)
}
func scanNamedToken(
@ -383,18 +361,26 @@ extension PureYAML.Parsing.Scanner {
value.append(character)
state.reader.advance()
}
let trimmed = trimTrailingSpaces(value)
appendMappingKeyIfNeeded(start: start, state: &state)
var trimmed = trimTrailingSpaces(value)
let isMappingKey = appendMappingKeyIfNeeded(start: start, state: &state)
if !isMappingKey {
trimmed = scanPlainScalarContinuations(
after: trimmed,
state: &state,
)
}
state.append(.scalar(value: trimmed, style: .plain), mark: start, endMark: state.reader.mark)
}
@discardableResult
func appendMappingKeyIfNeeded(
start: PureYAML.Parsing.Mark,
state: inout State,
) {
) -> Bool {
guard state.reader.peek() == ":", isMappingValueBoundary(state.reader.peek(offset: 1)) else {
return
return false
}
state.append(.mappingKey, mark: start, endMark: start)
return true
}
}

View File

@ -23,6 +23,14 @@ extension PureYAML.Parsing {
return tokens[previousIndex]
}
func peek(offset: Int = 1) -> Token? {
let targetIndex = index + offset
guard tokens.indices.contains(targetIndex) else {
return nil
}
return tokens[targetIndex]
}
mutating func advance() -> Token? {
guard let token = current else {
return nil

View File

@ -1,6 +1,7 @@
extension PureYAML.Parsing.TokenEventParser {
mutating func parseBlockSequence(
properties: PureYAML.Parsing.NodeProperties,
allowMappingKeyTerminator: Bool = false,
) throws -> PureYAML.Parsing.NodeResult {
let start = try expect("block sequence entry") { $0.isBlockEntry }
let startMark = properties.mark ?? start.mark
@ -12,12 +13,24 @@ extension PureYAML.Parsing.TokenEventParser {
mark: startMark,
))
try parseBlockSequenceItem(after: start, endMark: &endMark)
try parseBlockSequenceItem(
after: start,
endMark: &endMark,
allowMappingKeyTerminator: allowMappingKeyTerminator,
)
try consumeSequenceItemBoundaryDedent(sequenceIndentWidth: start.mark.column - 1)
try consumeSequenceItemBoundaryIndent(sequenceIndentWidth: start.mark.column - 1)
while cursor.current?.kind.isBlockEntry == true {
let entry = try expect("block sequence entry") { $0.isBlockEntry }
try parseBlockSequenceItem(after: entry, endMark: &endMark)
try parseBlockSequenceItem(
after: entry,
endMark: &endMark,
allowMappingKeyTerminator: allowMappingKeyTerminator,
)
try consumeSequenceItemBoundaryDedent(sequenceIndentWidth: entry.mark.column - 1)
try consumeSequenceItemBoundaryIndent(sequenceIndentWidth: entry.mark.column - 1)
}
if let current = cursor.current, current.kind.isMappingKey {
if let current = cursor.current, current.kind.isMappingKey, !allowMappingKeyTerminator {
throw PureYAML.Parsing.ParseError.mixedCollectionStyles(line: current.mark.line)
}
@ -25,9 +38,28 @@ extension PureYAML.Parsing.TokenEventParser {
return PureYAML.Parsing.NodeResult(kind: .sequence, endMark: endMark)
}
mutating func consumeSequenceItemBoundaryDedent(sequenceIndentWidth: Int) throws {
guard dedentWidth(cursor.current) == sequenceIndentWidth,
cursor.peek()?.kind.isBlockEntry == true
else {
return
}
_ = try expect("dedent") { $0.isDedent }
}
mutating func consumeSequenceItemBoundaryIndent(sequenceIndentWidth: Int) throws {
guard indentWidth(cursor.current) == sequenceIndentWidth,
cursor.peek()?.kind.isBlockEntry == true
else {
return
}
_ = try expect("indent") { $0.isIndent }
}
mutating func parseBlockSequenceItem(
after entry: PureYAML.Parsing.Token,
endMark: inout PureYAML.Parsing.Mark,
allowMappingKeyTerminator: Bool,
) throws {
if hasNodeOnSameLine(after: entry) {
let properties = consumeProperties()
@ -41,6 +73,12 @@ extension PureYAML.Parsing.TokenEventParser {
endMark = try parseBlockMapping(
properties: properties,
includeIndentedContinuation: true,
minimumColumn: entry.mark.column + 1,
).endMark
} else if cursor.current?.kind.isBlockEntry == true {
endMark = try parseBlockSequence(
properties: properties,
allowMappingKeyTerminator: allowMappingKeyTerminator,
).endMark
} else {
endMark = try parseNode(properties: properties).endMark
@ -59,8 +97,12 @@ extension PureYAML.Parsing.TokenEventParser {
mutating func parseBlockMapping(
properties: PureYAML.Parsing.NodeProperties,
includeIndentedContinuation: Bool,
minimumColumn: Int? = nil,
) throws -> PureYAML.Parsing.NodeResult {
let startMark = properties.mark ?? cursor.current?.mark ?? .start
let tokenColumn = cursor.current?.mark.column ?? startMark.column
let contentColumn = min(startMark.column, tokenColumn)
let boundaryColumn = minimumColumn ?? contentColumn
var endMark = startMark
events.append(.mappingStart(
anchor: properties.anchor,
@ -68,20 +110,13 @@ extension PureYAML.Parsing.TokenEventParser {
style: .block,
mark: startMark,
))
while cursor.current?.kind.isMappingKey == true {
endMark = try parseMappingPair().endMark
}
while includeIndentedContinuation, cursor.current?.kind.isIndent == true {
_ = try expect("indent") { $0.isIndent }
guard cursor.current?.kind.isMappingKey == true else {
throw unexpectedToken(expected: "mapping key")
}
while cursor.current?.kind.isMappingKey == true {
endMark = try parseMappingPair().endMark
}
_ = try expect("dedent") { $0.isDedent }
}
if let current = cursor.current, !canEndBlockMapping(at: current) {
try parseBlockMappingPairs(
boundaryColumn: boundaryColumn,
includeIndentedContinuation: includeIndentedContinuation,
endMark: &endMark,
)
if let current = cursor.current, !canEndBlockMapping(at: current, minimumColumn: boundaryColumn) {
throw PureYAML.Parsing.ParseError.unexpectedToken(
expected: "mapping key",
actual: current.kind.description,
@ -93,6 +128,57 @@ extension PureYAML.Parsing.TokenEventParser {
return PureYAML.Parsing.NodeResult(kind: .mapping, endMark: endMark)
}
mutating func parseBlockMappingPairs(
boundaryColumn: Int,
includeIndentedContinuation: Bool,
endMark: inout PureYAML.Parsing.Mark,
) throws {
while canContinueBlockMapping(minimumColumn: boundaryColumn) {
endMark = try parseMappingPairAndBoundaries(
boundaryColumn: boundaryColumn,
includeIndentedContinuation: includeIndentedContinuation,
).endMark
}
while includeIndentedContinuation, cursor.current?.kind.isIndent == true {
endMark = try parseIndentedBlockMappingPairs(boundaryColumn: boundaryColumn).endMark
}
}
mutating func parseIndentedBlockMappingPairs(
boundaryColumn: Int,
) throws -> PureYAML.Parsing.NodeResult {
_ = try expect("indent") { $0.isIndent }
guard cursor.current?.kind.isMappingKey == true else {
throw unexpectedToken(expected: "mapping key")
}
var endMark = cursor.current?.mark ?? .start
while canContinueBlockMapping(minimumColumn: boundaryColumn) {
endMark = try parseMappingPairAndBoundaries(
boundaryColumn: boundaryColumn,
includeIndentedContinuation: true,
).endMark
}
if !canEndBlockMapping(at: cursor.current, minimumColumn: boundaryColumn) {
_ = try expect("dedent") { $0.isDedent }
}
return PureYAML.Parsing.NodeResult(kind: .mapping, endMark: endMark)
}
mutating func parseMappingPairAndBoundaries(
boundaryColumn: Int,
includeIndentedContinuation: Bool,
) throws -> PureYAML.Parsing.NodeResult {
let result = try parseMappingPair()
try consumeMappingSiblingBoundaryDedents(minimumColumn: boundaryColumn)
try consumeMappingSiblingBoundaryReindent(minimumColumn: boundaryColumn)
if includeIndentedContinuation {
try consumeDedents(atOrAbove: boundaryColumn)
try consumeMappingSiblingBoundaryDedents(minimumColumn: boundaryColumn)
try consumeMappingSiblingBoundaryReindent(minimumColumn: boundaryColumn)
}
return result
}
mutating func parseBlockScalar(
properties: PureYAML.Parsing.NodeProperties,
) throws -> PureYAML.Parsing.NodeResult {

View File

@ -12,19 +12,71 @@ extension PureYAML.Parsing.TokenEventParser {
let properties = consumeProperties()
if cursor.current?.kind.isIndent == true {
_ = try expect("indent") { $0.isIndent }
let result = try parseNode(properties: properties)
_ = try expect("dedent") { $0.isDedent }
let result: PureYAML.Parsing.NodeResult = if cursor.current?.kind.isBlockEntry == true {
try parseBlockSequence(
properties: properties,
allowMappingKeyTerminator: true,
)
} else {
try parseNode(properties: properties)
}
if cursor.current?.kind.isDedent == true {
_ = try expect("dedent") { $0.isDedent }
} else if cursor.current?.kind.isMappingKey != true {
_ = try expect("dedent") { $0.isDedent }
}
return result
}
if properties == .none, canParsePlainScalarWithContinuation(after: valueIndicator) {
return try parsePlainScalarWithContinuation(properties: properties)
}
if properties != .none || hasNodeOnSameLine(after: valueIndicator) {
return try parseNode(properties: properties)
}
if cursor.current?.kind.isBlockEntry == true {
return try parseBlockSequence(
properties: properties,
allowMappingKeyTerminator: true,
)
}
let mark = valueIndicator.endMark
events.append(.scalar(value: "", anchor: nil, tag: nil, style: .plain, mark: mark))
return PureYAML.Parsing.NodeResult(kind: .scalar, endMark: mark)
}
mutating func parsePlainScalarWithContinuation(
properties: PureYAML.Parsing.NodeProperties,
) throws -> PureYAML.Parsing.NodeResult {
let first = try expect("plain scalar") { kind in
if case .scalar(_, .plain) = kind {
return true
}
return false
}
var value = try decodedScalar(from: first)
var endMark = first.endMark
_ = try expect("indent") { $0.isIndent }
while isScalar(cursor.current) {
let token = cursor.advance() ?? first
let continuation = try plainContinuationText(from: token)
value += " " + continuation
endMark = token.endMark
}
_ = try expect("dedent") { $0.isDedent }
let mark = properties.mark ?? first.mark
events.append(.scalar(
value: value,
anchor: properties.anchor,
tag: properties.tag,
style: .plain,
mark: mark,
))
return PureYAML.Parsing.NodeResult(kind: .scalar, endMark: endMark)
}
mutating func consumeProperties() -> PureYAML.Parsing.NodeProperties {
var properties = PureYAML.Parsing.NodeProperties.none
while let token = cursor.current {
@ -61,6 +113,27 @@ extension PureYAML.Parsing.TokenEventParser {
}
}
func decodedScalar(from token: PureYAML.Parsing.Token) throws -> String {
guard case let .scalar(value, style) = token.kind else {
throw unexpectedToken(expected: "scalar")
}
return try decodeScalar(value, style: style, line: token.mark.line)
}
func plainContinuationText(from token: PureYAML.Parsing.Token) throws -> String {
guard case let .scalar(value, style) = token.kind else {
throw unexpectedToken(expected: "scalar")
}
switch style {
case .doubleQuoted:
return "\"\(value)\""
case .singleQuoted:
return "'\(value)'"
case .folded, .literal, .plain:
return try decodeScalar(value, style: style, line: token.mark.line)
}
}
func hasNodeOnSameLine(after token: PureYAML.Parsing.Token) -> Bool {
guard let current = cursor.current, !current.kind.isTerminator else {
return false
@ -68,15 +141,129 @@ extension PureYAML.Parsing.TokenEventParser {
return current.mark.line == token.mark.line
}
func canEndBlockMapping(at token: PureYAML.Parsing.Token) -> Bool {
token.kind.isBlockEntry
func canParsePlainScalarWithContinuation(after token: PureYAML.Parsing.Token) -> Bool {
guard case .scalar(_, .plain) = cursor.current?.kind else {
return false
}
guard cursor.current?.mark.line == token.mark.line else {
return false
}
guard cursor.peek()?.kind.isIndent == true else {
return false
}
guard isScalar(cursor.peek(offset: 2)) else {
return false
}
return true
}
func canParsePlainScalarContinuation() -> Bool {
guard case .scalar(_, .plain) = cursor.current?.kind else {
return false
}
guard cursor.peek()?.kind.isIndent == true else {
return false
}
guard isScalar(cursor.peek(offset: 2)) else {
return false
}
return true
}
func isScalar(_ token: PureYAML.Parsing.Token?) -> Bool {
guard case .scalar = token?.kind else {
return false
}
return true
}
mutating func consumeDedents(atOrAbove minimumColumn: Int) throws {
while let width = dedentWidth(cursor.current), width >= minimumColumn {
_ = try expect("dedent") { $0.isDedent }
}
}
mutating func consumeMappingSiblingBoundaryDedents(minimumColumn: Int) throws {
while canConsumeMappingSiblingBoundaryDedent(minimumColumn: minimumColumn) {
_ = try expect("dedent") { $0.isDedent }
}
}
func canConsumeMappingSiblingBoundaryDedent(minimumColumn: Int) -> Bool {
guard let width = dedentWidth(cursor.current),
let next = cursor.peek(),
next.kind.isMappingKey,
next.mark.column >= minimumColumn
else {
return false
}
return width == next.mark.column - 1
}
mutating func consumeMappingSiblingBoundaryReindent(minimumColumn: Int) throws {
guard let dedent = dedentWidth(cursor.current),
dedent < minimumColumn,
indentWidth(cursor.peek()) == minimumColumn,
let mappingKey = cursor.peek(offset: 2),
mappingKey.kind.isMappingKey,
mappingKey.mark.column == minimumColumn + 1
else {
return
}
_ = try expect("dedent") { $0.isDedent }
_ = try expect("indent") { $0.isIndent }
}
func dedentWidth(_ token: PureYAML.Parsing.Token?) -> Int? {
guard case let .dedent(width) = token?.kind else {
return nil
}
return width
}
func indentWidth(_ token: PureYAML.Parsing.Token?) -> Int? {
guard case let .indent(width) = token?.kind else {
return nil
}
return width
}
func canContinueBlockMapping(minimumColumn: Int?) -> Bool {
guard let current = cursor.current, current.kind.isMappingKey else {
return false
}
guard let minimumColumn else {
return true
}
return current.mark.column >= minimumColumn
}
func canEndBlockMapping(
at token: PureYAML.Parsing.Token?,
minimumColumn: Int? = nil,
) -> Bool {
guard let token else {
return true
}
return token.kind.isBlockEntry
|| token.kind.isDedent
|| token.kind.isDocumentEnd
|| token.kind.isDocumentStart
|| token.kind.isFlowTerminator
|| isLowerColumnMappingKey(token, minimumColumn: minimumColumn)
|| token.kind.isStreamEnd
}
func isLowerColumnMappingKey(
_ token: PureYAML.Parsing.Token?,
minimumColumn: Int?,
) -> Bool {
guard let token, token.kind.isMappingKey, let minimumColumn else {
return false
}
return token.mark.column < minimumColumn
}
mutating func expect(
_ expected: String,
matching: (PureYAML.Parsing.TokenKind) -> Bool,

View File

@ -62,6 +62,10 @@ extension PureYAML.Parsing.TokenEventParser {
let result = try parseNode()
var endMark = result.endMark
while cursor.current?.kind.isDedent == true {
let dedent = try expect("dedent") { $0.isDedent }
endMark = dedent.endMark
}
if cursor.current?.kind.isDocumentEnd == true {
let end = try expect("document end") { $0.isDocumentEnd }
endMark = end.endMark
@ -129,6 +133,9 @@ extension PureYAML.Parsing.TokenEventParser {
includeIndentedContinuation: false,
)
case let .scalar(value, style):
if canParsePlainScalarContinuation() {
return try parsePlainScalarWithContinuation(properties: properties)
}
_ = cursor.advance()
let decoded = try decodeScalar(value, style: style, line: token.mark.line)
let mark = properties.mark ?? token.mark

View File

@ -0,0 +1,38 @@
# Real YAML Fixture Manifest
These fixtures are vendored test inputs. They are pinned to immutable GitHub
commit SHAs so compatibility failures can be reproduced without network access.
The files are not implementation source and are used only by
`RealYAMLFixtureTests`.
| Local file | Size | Lines | Bytes | Upstream license | Pinned source |
|---|---:|---:|---:|---|---|
| `real-yaml/kubernetes-simple-pod.yaml` | very short | 12 | 186 | MIT | `ContainerSolutions/kubernetes-examples@fa6e84542f4350b97991bfffae553d248953e443/Pod/simple.yaml` |
| `real-yaml/kustomize-wordpress.yaml` | very short | 18 | 262 | Apache-2.0 | `kubernetes-sigs/kustomize@313aacedd3adcb6564fd362bcef52f5aa3abef85/examples/wordpress/kustomization.yaml` |
| `real-yaml/docker-compose-nginx-golang-postgres.yaml` | short | 48 | 852 | CC0-1.0 | `docker/awesome-compose@8ded149643ef0ba21fce5a5d39894b39fff6c7ee/nginx-golang-postgres/compose.yaml` |
| `real-yaml/github-actions-swift-format.yml` | short | 84 | 3591 | Apache-2.0 | `swiftlang/swift-format@fffd8df6153456838039aae4b9b901f6f31f5687/.github/workflows/pull_request.yml` |
| `real-yaml/openapi-petstore.yaml` | short | 119 | 2768 | Apache-2.0 | `OAI/OpenAPI-Specification@18c3204bc55c8c75fe25a039a3cf085f0e732208/_archive_/schemas/v3.0/pass/petstore.yaml` |
| `real-yaml/prow-cluster-api-presubmits.yaml` | medium | 427 | 14213 | Apache-2.0 | `kubernetes/test-infra@9001df901ba65b1b74d16be5cc910e68a5f35b91/config/jobs/kubernetes-sigs/cluster-api/cluster-api-release-1-13-presubmits.yaml` |
| `real-yaml/prometheus-conf-good.yml` | medium | 481 | 11562 | Apache-2.0 | `prometheus/prometheus@5e3c892bfc1209b739562f885a8cfa17c6cd7fa2/config/testdata/conf.good.yml` |
| `real-yaml/cert-manager-certificate-crd.yaml` | large | 883 | 44059 | Apache-2.0 | `cert-manager/cert-manager@59c5f5361a5b1cd2d0ca84cd9983a3beac3846bd/deploy/crds/cert-manager.io_certificates.yaml` |
| `real-yaml/cert-manager-values.yaml` | large | 1759 | 65536 | Apache-2.0 | `cert-manager/cert-manager@59c5f5361a5b1cd2d0ca84cd9983a3beac3846bd/deploy/charts/cert-manager/values.yaml` |
| `real-yaml/github-rest-api.yaml` | extreme | 257280 | 9549118 | MIT | `github/rest-api-description@0d4e436c347b444cd71b4eb1bd73948fd51c3402/descriptions-next/api.github.com/api.github.com.yaml` |
Selection covers GitHub Actions, Docker Compose, Kubernetes manifests,
Kustomize, OpenAPI, Prow jobs, Prometheus config, Helm values, CRDs, and one
extreme OpenAPI document. If a fixture is parseable by mature YAML tools, this
suite expects PureYAML to parse it or to pin an exact unsupported feature error
with a linked issue.
## Validation Failure Fixtures
These fixtures are intentionally valid YAML that should fail validation. They
exist to demonstrate exact validation output for user-facing diagnostic reports,
strict throwing behavior, non-strict warning returns, path rendering, required
presence checks, and required absence checks.
| Local file | Purpose |
|---|---|
| `validation-failures/duplicate-keys.yaml` | Default duplicate-key diagnostics with root and punctuation-heavy nested keys. |
| `validation-failures/missing-required-and-forbidden.yaml` | Custom required-presence plus required-absence diagnostics in one document. |
| `validation-failures/warnings.yaml` | Warning-only diagnostics that throw in strict mode and return in non-strict mode. |

View File

@ -0,0 +1,883 @@
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.21.0
name: certificates.cert-manager.io
spec:
group: cert-manager.io
names:
categories:
- cert-manager
kind: Certificate
listKind: CertificateList
plural: certificates
shortNames:
- cert
- certs
singular: certificate
scope: Namespaced
versions:
- additionalPrinterColumns:
- jsonPath: .status.conditions[?(@.type == "Ready")].status
name: Ready
type: string
- jsonPath: .spec.secretName
name: Secret
type: string
- jsonPath: .spec.issuerRef.name
name: Issuer
priority: 1
type: string
- jsonPath: .status.conditions[?(@.type == "Ready")].message
name: Status
priority: 1
type: string
- description: CreationTimestamp is a timestamp representing the server time when
this object was created. It is not guaranteed to be set in happens-before
order across separate operations. Clients may not set this value. It is represented
in RFC3339 form and is in UTC.
jsonPath: .metadata.creationTimestamp
name: Age
type: date
name: v1
schema:
openAPIV3Schema:
description: |-
A Certificate resource should be created to ensure an up to date and signed
X.509 certificate is stored in the Kubernetes Secret resource named in `spec.secretName`.
The stored certificate will be renewed before it expires (as configured by `spec.renewBefore`).
properties:
apiVersion:
description: |-
APIVersion defines the versioned schema of this representation of an object.
Servers should convert recognized schemas to the latest internal value, and
may reject unrecognized values.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
type: string
kind:
description: |-
Kind is a string value representing the REST resource this object represents.
Servers may infer this from the endpoint the client submits requests to.
Cannot be updated.
In CamelCase.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
type: string
metadata:
type: object
spec:
description: |-
Specification of the desired state of the Certificate resource.
https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status
properties:
additionalOutputFormats:
description: |-
Defines extra output formats of the private key and signed certificate chain
to be written to this Certificate's target Secret.
items:
description: |-
CertificateAdditionalOutputFormat defines an additional output format of a
Certificate resource. These contain supplementary data formats of the signed
certificate chain and paired private key.
properties:
type:
description: |-
Type is the name of the format type that should be written to the
Certificate's target Secret.
enum:
- DER
- CombinedPEM
type: string
required:
- type
type: object
type: array
x-kubernetes-list-type: atomic
commonName:
description: |-
Requested common name X509 certificate subject attribute.
More info: https://datatracker.ietf.org/doc/html/rfc5280#section-4.1.2.6
NOTE: TLS clients will ignore this value when any subject alternative name is
set (see https://tools.ietf.org/html/rfc6125#section-6.4.4).
Should have a length of 64 characters or fewer to avoid generating invalid CSRs.
Cannot be set if the `literalSubject` field is set.
type: string
dnsNames:
description: Requested DNS subject alternative names.
items:
type: string
type: array
x-kubernetes-list-type: atomic
duration:
description: |-
Requested 'duration' (i.e. lifetime) of the Certificate. Note that the
issuer may choose to ignore the requested duration, just like any other
requested attribute.
If unset, this defaults to 90 days.
Minimum accepted duration is 1 hour.
Value must be in units accepted by Go time.ParseDuration https://golang.org/pkg/time/#ParseDuration.
type: string
emailAddresses:
description: Requested email subject alternative names.
items:
type: string
type: array
x-kubernetes-list-type: atomic
encodeUsagesInRequest:
description: |-
Whether the KeyUsage and ExtKeyUsage extensions should be set in the encoded CSR.
This option defaults to true, and should only be disabled if the target
issuer does not support CSRs with these X509 KeyUsage/ ExtKeyUsage extensions.
type: boolean
ipAddresses:
description: Requested IP address subject alternative names.
items:
type: string
type: array
x-kubernetes-list-type: atomic
isCA:
description: |-
Requested basic constraints isCA value.
The isCA value is used to set the `isCA` field on the created CertificateRequest
resources. Note that the issuer may choose to ignore the requested isCA value, just
like any other requested attribute.
If true, this will automatically add the `cert sign` usage to the list
of requested `usages`.
type: boolean
issuerRef:
description: |-
Reference to the issuer responsible for issuing the certificate.
If the issuer is namespace-scoped, it must be in the same namespace
as the Certificate. If the issuer is cluster-scoped, it can be used
from any namespace.
The `name` field of the reference must always be specified.
properties:
group:
description: |-
Group of the issuer being referred to.
Defaults to 'cert-manager.io'.
type: string
kind:
description: |-
Kind of the issuer being referred to.
Defaults to 'Issuer'.
type: string
name:
description: Name of the issuer being referred to.
type: string
required:
- name
type: object
keystores:
description: Additional keystore output formats to be stored in the
Certificate's Secret.
properties:
jks:
description: |-
JKS configures options for storing a JKS keystore in the
`spec.secretName` Secret resource.
properties:
alias:
description: |-
Alias specifies the alias of the key in the keystore, required by the JKS format.
If not provided, the default alias `certificate` will be used.
type: string
create:
description: |-
Create enables JKS keystore creation for the Certificate.
If true, a file named `keystore.jks` will be created in the target
Secret resource, encrypted using the password stored in
`passwordSecretRef` or `password`.
The keystore file will be updated immediately.
If the issuer provided a CA certificate, a file named `truststore.jks`
will also be created in the target Secret resource, encrypted using the
password stored in `passwordSecretRef`
containing the issuing Certificate Authority
type: boolean
password:
description: |-
Password provides a literal password used to encrypt the JKS keystore.
Mutually exclusive with passwordSecretRef.
One of password or passwordSecretRef must provide a password with a non-zero length.
type: string
passwordSecretRef:
description: |-
PasswordSecretRef is a reference to a non-empty key in a Secret resource
containing the password used to encrypt the JKS keystore.
Mutually exclusive with password.
One of password or passwordSecretRef must provide a password with a non-zero length.
properties:
key:
description: |-
The key of the entry in the Secret resource's `data` field to be used.
Some instances of this field may be defaulted, in others it may be
required.
type: string
name:
description: |-
Name of the resource being referred to.
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
type: string
required:
- name
type: object
required:
- create
type: object
pkcs12:
description: |-
PKCS12 configures options for storing a PKCS12 keystore in the
`spec.secretName` Secret resource.
properties:
create:
description: |-
Create enables PKCS12 keystore creation for the Certificate.
If true, a file named `keystore.p12` will be created in the target
Secret resource, encrypted using the password stored in
`passwordSecretRef` or in `password`.
The keystore file will be updated immediately.
If the issuer provided a CA certificate, a file named `truststore.p12` will
also be created in the target Secret resource, encrypted using the
password stored in `passwordSecretRef` containing the issuing Certificate
Authority
type: boolean
password:
description: |-
Password provides a literal password used to encrypt the PKCS#12 keystore.
Mutually exclusive with passwordSecretRef.
One of password or passwordSecretRef must provide a password with a non-zero length.
type: string
passwordSecretRef:
description: |-
PasswordSecretRef is a reference to a non-empty key in a Secret resource
containing the password used to encrypt the PKCS#12 keystore.
Mutually exclusive with password.
One of password or passwordSecretRef must provide a password with a non-zero length.
properties:
key:
description: |-
The key of the entry in the Secret resource's `data` field to be used.
Some instances of this field may be defaulted, in others it may be
required.
type: string
name:
description: |-
Name of the resource being referred to.
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
type: string
required:
- name
type: object
profile:
description: |-
Profile specifies the key and certificate encryption algorithms and the HMAC algorithm
used to create the PKCS12 keystore. Default value is `LegacyRC2` for backward compatibility.
If provided, allowed values are:
`LegacyRC2`: Deprecated. Not supported by default in OpenSSL 3 or Java 20.
`LegacyDES`: Less secure algorithm. Use this option for maximal compatibility.
`Modern2023`: Secure algorithm. Use this option in case you have to always use secure algorithms
(e.g., because of company policy). Please note that the security of the algorithm is not that important
in reality, because the unencrypted certificate and private key are also stored in the Secret.
`Modern2026`: Encodes PKCS#12 files using algorithms that are considered modern as of 2026.
Private keys and certificates are encrypted using PBES2 with PBKDF2-HMAC-SHA-256 and AES-256-CBC.
The MAC algorithm is PBMAC1 with PBKDF2-HMAC-SHA-256 and HMAC-SHA256.
Files produced with this profile can be read by OpenSSL 3.4.0 and higher, Java 26 and higher,
or with Java using compatible versions of Bouncy Castle. Meets FIPS 140-3 requirements.
enum:
- LegacyRC2
- LegacyDES
- Modern2023
- Modern2026
type: string
required:
- create
type: object
type: object
literalSubject:
description: |-
Requested X.509 certificate subject, represented using the LDAP "String
Representation of a Distinguished Name" [1].
Important: the LDAP string format also specifies the order of the attributes
in the subject, this is important when issuing certs for LDAP authentication.
Example: `CN=foo,DC=corp,DC=example,DC=com`
More info [1]: https://datatracker.ietf.org/doc/html/rfc4514
More info: https://github.com/cert-manager/cert-manager/issues/3203
More info: https://github.com/cert-manager/cert-manager/issues/4424
Cannot be set if the `subject` or `commonName` field is set.
type: string
nameConstraints:
description: |-
x.509 certificate NameConstraint extension which MUST NOT be used in a non-CA certificate.
More Info: https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.10
This is an Alpha Feature and is only enabled with the
`--feature-gates=NameConstraints=true` option set on both
the controller and webhook components.
properties:
critical:
description: if true then the name constraints are marked critical.
type: boolean
excluded:
description: |-
Excluded contains the constraints which must be disallowed. Any name matching a
restriction in the excluded field is invalid regardless
of information appearing in the permitted
properties:
dnsDomains:
description: DNSDomains is a list of DNS domains that are
permitted or excluded.
items:
type: string
type: array
x-kubernetes-list-type: atomic
emailAddresses:
description: EmailAddresses is a list of Email Addresses that
are permitted or excluded.
items:
type: string
type: array
x-kubernetes-list-type: atomic
ipRanges:
description: |-
IPRanges is a list of IP Ranges that are permitted or excluded.
This should be a valid CIDR notation.
items:
type: string
type: array
x-kubernetes-list-type: atomic
uriDomains:
description: URIDomains is a list of URI domains that are
permitted or excluded.
items:
type: string
type: array
x-kubernetes-list-type: atomic
type: object
permitted:
description: Permitted contains the constraints in which the names
must be located.
properties:
dnsDomains:
description: DNSDomains is a list of DNS domains that are
permitted or excluded.
items:
type: string
type: array
x-kubernetes-list-type: atomic
emailAddresses:
description: EmailAddresses is a list of Email Addresses that
are permitted or excluded.
items:
type: string
type: array
x-kubernetes-list-type: atomic
ipRanges:
description: |-
IPRanges is a list of IP Ranges that are permitted or excluded.
This should be a valid CIDR notation.
items:
type: string
type: array
x-kubernetes-list-type: atomic
uriDomains:
description: URIDomains is a list of URI domains that are
permitted or excluded.
items:
type: string
type: array
x-kubernetes-list-type: atomic
type: object
type: object
otherNames:
description: |-
`otherNames` is an escape hatch for SAN that allows any type. We currently restrict the support to string like otherNames, cf RFC 5280 p 37
Any UTF8 String valued otherName can be passed with by setting the keys oid: x.x.x.x and UTF8Value: somevalue for `otherName`.
Most commonly this would be UPN set with oid: 1.3.6.1.4.1.311.20.2.3
You should ensure that any OID passed is valid for the UTF8String type as we do not explicitly validate this.
items:
properties:
oid:
description: |-
OID is the object identifier for the otherName SAN.
The object identifier must be expressed as a dotted string, for
example, "1.2.840.113556.1.4.221".
type: string
utf8Value:
description: |-
utf8Value is the string value of the otherName SAN.
The utf8Value accepts any valid UTF8 string to set as value for the otherName SAN.
type: string
type: object
type: array
x-kubernetes-list-type: atomic
privateKey:
description: |-
Private key options. These include the key algorithm and size, the used
encoding and the rotation policy.
properties:
algorithm:
description: |-
Algorithm is the private key algorithm of the corresponding private key
for this certificate.
If provided, allowed values are either `RSA`, `ECDSA` or `Ed25519`.
If `algorithm` is specified and `size` is not provided,
key size of 2048 will be used for `RSA` key algorithm and
key size of 256 will be used for `ECDSA` key algorithm.
key size is ignored when using the `Ed25519` key algorithm.
enum:
- RSA
- ECDSA
- Ed25519
type: string
encoding:
description: |-
The private key cryptography standards (PKCS) encoding for this
certificate's private key to be encoded in.
If provided, allowed values are `PKCS1` and `PKCS8` standing for PKCS#1
and PKCS#8, respectively.
Defaults to `PKCS1` if not specified.
enum:
- PKCS1
- PKCS8
type: string
rotationPolicy:
description: |-
RotationPolicy controls how private keys should be regenerated when a
re-issuance is being processed.
If set to `Never`, a private key will only be generated if one does not
already exist in the target `spec.secretName`. If one does exist but it
does not have the correct algorithm or size, a warning will be raised
to await user intervention.
If set to `Always`, a private key matching the specified requirements
will be generated whenever a re-issuance occurs.
Default is `Always`.
The default was changed from `Never` to `Always` in cert-manager >=v1.18.0.
enum:
- Never
- Always
type: string
size:
description: |-
Size is the key bit size of the corresponding private key for this certificate.
If `algorithm` is set to `RSA`, valid values are `2048`, `4096` or `8192`,
and will default to `2048` if not specified.
If `algorithm` is set to `ECDSA`, valid values are `256`, `384` or `521`,
and will default to `256` if not specified.
If `algorithm` is set to `Ed25519`, Size is ignored.
No other values are allowed.
type: integer
type: object
renewBefore:
description: |-
How long before the currently issued certificate's expiry cert-manager should
renew the certificate. For example, if a certificate is valid for 60 minutes,
and `renewBefore=10m`, cert-manager will begin to attempt to renew the certificate
50 minutes after it was issued (i.e. when there are 10 minutes remaining until
the certificate is no longer valid).
NOTE: The actual lifetime of the issued certificate is used to determine the
renewal time. If an issuer returns a certificate with a different lifetime than
the one requested, cert-manager will use the lifetime of the issued certificate.
If unset, this defaults to 1/3 of the issued certificate's lifetime.
Minimum accepted value is 5 minutes.
Value must be in units accepted by Go time.ParseDuration https://golang.org/pkg/time/#ParseDuration.
Cannot be set if the `renewBeforePercentage` field is set.
type: string
renewBeforePercentage:
description: |-
`renewBeforePercentage` is like `renewBefore`, except it is a relative percentage
rather than an absolute duration. For example, if a certificate is valid for 60
minutes, and `renewBeforePercentage=25`, cert-manager will begin to attempt to
renew the certificate 45 minutes after it was issued (i.e. when there are 15
minutes (25%) remaining until the certificate is no longer valid).
NOTE: The actual lifetime of the issued certificate is used to determine the
renewal time. If an issuer returns a certificate with a different lifetime than
the one requested, cert-manager will use the lifetime of the issued certificate.
Value must be an integer in the range (0,100). The minimum effective
`renewBefore` derived from the `renewBeforePercentage` and `duration` fields is 5
minutes.
Cannot be set if the `renewBefore` field is set.
format: int32
type: integer
renewal:
description: |-
`renewal` allows configuration of how your certificate is renewed. If the policy mentioned is
`RenewBefore` then the controller respects `renewBefore` and `renewBeforePercentage`.
properties:
policy:
description: '`policy` must be one of `Disabled`, `RenewBefore`.'
enum:
- RenewBefore
- Disabled
type: string
windows:
description: '`windows` mentions the behavior of when the renewal
must happen.'
items:
description: CertificateRenewalWindows is the definition for
renewal windows
properties:
cron:
description: |-
`cron` is a cron compliant string to allow when the renewal should be allowed. Format is as shown below:
* * * * *
| | | | |
| | | | day of the week (0–6) (Sunday to Saturday;
| | | month (1–12) 7 is also Sunday on some systems)
| | day of the month (1–31)
| hour (0–23)
minute (0–59)
minLength: 1
type: string
timezone:
description: |-
`timezone` is IANA compliant timezone. For example America/Denver.
If this field is not set, timezone is treated as UTC.
minLength: 1
type: string
windowDuration:
description: |-
`windowDuration` is how long the cron definition is active for.
Value must be in units accepted by Go time.ParseDuration https://golang.org/pkg/time/#ParseDuration.
pattern: ^([0-9]+(\.[0-9]+)?(s|m|h))+$
type: string
required:
- cron
- windowDuration
type: object
type: array
x-kubernetes-list-type: atomic
type: object
revisionHistoryLimit:
description: |-
The maximum number of CertificateRequest revisions that are maintained in
the Certificate's history. Each revision represents a single `CertificateRequest`
created by this Certificate, either when it was created, renewed, or Spec
was changed. Revisions will be removed by oldest first if the number of
revisions exceeds this number.
If set, revisionHistoryLimit must be a value of `1` or greater.
Default value is `1`.
format: int32
type: integer
secretName:
description: |-
Name of the Secret resource that will be automatically created and
managed by this Certificate resource. It will be populated with a
private key and certificate, signed by the denoted issuer. The Secret
resource lives in the same namespace as the Certificate resource.
type: string
secretTemplate:
description: |-
Defines annotations and labels to be copied to the Certificate's Secret.
Labels and annotations on the Secret will be changed as they appear on the
SecretTemplate when added or removed. SecretTemplate annotations are added
in conjunction with, and cannot overwrite, the base set of annotations
cert-manager sets on the Certificate's Secret.
properties:
annotations:
additionalProperties:
type: string
description: Annotations is a key value map to be copied to the
target Kubernetes Secret.
type: object
labels:
additionalProperties:
type: string
description: Labels is a key value map to be copied to the target
Kubernetes Secret.
type: object
type: object
signatureAlgorithm:
description: |-
Signature algorithm to use.
Allowed values for RSA keys: SHA256WithRSA, SHA384WithRSA, SHA512WithRSA.
Allowed values for ECDSA keys: ECDSAWithSHA256, ECDSAWithSHA384, ECDSAWithSHA512.
Allowed values for Ed25519 keys: PureEd25519.
enum:
- SHA256WithRSA
- SHA384WithRSA
- SHA512WithRSA
- ECDSAWithSHA256
- ECDSAWithSHA384
- ECDSAWithSHA512
- PureEd25519
type: string
subject:
description: |-
Requested set of X509 certificate subject attributes.
More info: https://datatracker.ietf.org/doc/html/rfc5280#section-4.1.2.6
The common name attribute is specified separately in the `commonName` field.
Cannot be set if the `literalSubject` field is set.
properties:
countries:
description: Countries to be used on the Certificate.
items:
type: string
type: array
x-kubernetes-list-type: atomic
localities:
description: Cities to be used on the Certificate.
items:
type: string
type: array
x-kubernetes-list-type: atomic
organizationalUnits:
description: Organizational Units to be used on the Certificate.
items:
type: string
type: array
x-kubernetes-list-type: atomic
organizations:
description: Organizations to be used on the Certificate.
items:
type: string
type: array
x-kubernetes-list-type: atomic
postalCodes:
description: Postal codes to be used on the Certificate.
items:
type: string
type: array
x-kubernetes-list-type: atomic
provinces:
description: State/Provinces to be used on the Certificate.
items:
type: string
type: array
x-kubernetes-list-type: atomic
serialNumber:
description: Serial number to be used on the Certificate.
type: string
streetAddresses:
description: Street addresses to be used on the Certificate.
items:
type: string
type: array
x-kubernetes-list-type: atomic
type: object
uris:
description: Requested URI subject alternative names.
items:
type: string
type: array
x-kubernetes-list-type: atomic
usages:
description: |-
Requested key usages and extended key usages.
These usages are used to set the `usages` field on the created CertificateRequest
resources. If `encodeUsagesInRequest` is unset or set to `true`, the usages
will additionally be encoded in the `request` field which contains the CSR blob.
If unset, defaults to `digital signature` and `key encipherment`.
items:
description: |-
KeyUsage specifies valid usage contexts for keys.
See:
https://tools.ietf.org/html/rfc5280#section-4.2.1.3
https://tools.ietf.org/html/rfc5280#section-4.2.1.12
Valid KeyUsage values are as follows:
"signing",
"digital signature",
"content commitment",
"key encipherment",
"key agreement",
"data encipherment",
"cert sign",
"crl sign",
"encipher only",
"decipher only",
"any",
"server auth",
"client auth",
"code signing",
"email protection",
"s/mime",
"ipsec end system",
"ipsec tunnel",
"ipsec user",
"timestamping",
"ocsp signing",
"microsoft sgc",
"netscape sgc"
enum:
- signing
- digital signature
- content commitment
- key encipherment
- key agreement
- data encipherment
- cert sign
- crl sign
- encipher only
- decipher only
- any
- server auth
- client auth
- code signing
- email protection
- s/mime
- ipsec end system
- ipsec tunnel
- ipsec user
- timestamping
- ocsp signing
- microsoft sgc
- netscape sgc
type: string
type: array
x-kubernetes-list-type: atomic
required:
- issuerRef
- secretName
type: object
status:
description: |-
Status of the Certificate.
This is set and managed automatically.
Read-only.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status
properties:
conditions:
description: |-
List of status conditions to indicate the status of certificates.
Known condition types are `Ready` and `Issuing`.
items:
description: CertificateCondition contains condition information
for a Certificate.
properties:
lastTransitionTime:
description: |-
LastTransitionTime is the timestamp corresponding to the last status
change of this condition.
format: date-time
type: string
message:
description: |-
Message is a human readable description of the details of the last
transition, complementing reason.
type: string
observedGeneration:
description: |-
If set, this represents the .metadata.generation that the condition was
set based upon.
For instance, if .metadata.generation is currently 12, but the
.status.condition[x].observedGeneration is 9, the condition is out of date
with respect to the current state of the Certificate.
format: int64
type: integer
reason:
description: |-
Reason is a brief machine readable explanation for the condition's last
transition.
type: string
status:
description: Status of the condition, one of (`True`, `False`,
`Unknown`).
enum:
- "True"
- "False"
- Unknown
type: string
type:
description: Type of the condition, known values are (`Ready`,
`Issuing`).
type: string
required:
- status
- type
type: object
type: array
x-kubernetes-list-map-keys:
- type
x-kubernetes-list-type: map
failedIssuanceAttempts:
description: |-
The number of continuous failed issuance attempts up till now. This
field gets removed (if set) on a successful issuance and gets set to
1 if unset and an issuance has failed. If an issuance has failed, the
delay till the next issuance will be calculated using formula
time.Hour * 2 ^ (failedIssuanceAttempts - 1).
type: integer
lastFailureTime:
description: |-
LastFailureTime is set only if the latest issuance for this
Certificate failed and contains the time of the failure. If an
issuance has failed, the delay till the next issuance will be
calculated using formula time.Hour * 2 ^ (failedIssuanceAttempts -
1). If the latest issuance has succeeded this field will be unset.
format: date-time
type: string
nextPrivateKeySecretName:
description: |-
The name of the Secret resource containing the private key to be used
for the next certificate iteration.
The keymanager controller will automatically set this field if the
`Issuing` condition is set to `True`.
It will automatically unset this field when the Issuing condition is
not set or False.
type: string
notAfter:
description: |-
The expiration time of the certificate stored in the secret named
by this resource in `spec.secretName`.
format: date-time
type: string
notBefore:
description: |-
The time after which the certificate stored in the secret named
by this resource in `spec.secretName` is valid.
format: date-time
type: string
renewalTime:
description: |-
RenewalTime is the time at which the certificate will be next
renewed.
If not set, no upcoming renewal is scheduled.
format: date-time
type: string
revision:
description: |-
The current 'revision' of the certificate as issued.
When a CertificateRequest resource is created, it will have the
`cert-manager.io/certificate-revision` set to one greater than the
current value of this field.
Upon issuance, this field will be set to the value of the annotation
on the CertificateRequest resource used to issue the certificate.
Persisting the value on the CertificateRequest resource allows the
certificates controller to know whether a request is part of an old
issuance or if it is part of the ongoing revision's issuance by
checking if the revision value in the annotation is greater than this
field.
type: integer
type: object
type: object
selectableFields:
- jsonPath: .spec.issuerRef.group
- jsonPath: .spec.issuerRef.kind
- jsonPath: .spec.issuerRef.name
served: true
storage: true
subresources:
status: {}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,48 @@
services:
backend:
build:
context: backend
target: builder
secrets:
- db-password
depends_on:
db:
condition: service_healthy
db:
image: postgres
restart: always
user: postgres
secrets:
- db-password
volumes:
- db-data:/var/lib/postgresql/data
environment:
- POSTGRES_DB=example
- POSTGRES_PASSWORD_FILE=/run/secrets/db-password
expose:
- 5432
healthcheck:
test: [ "CMD", "pg_isready" ]
interval: 10s
timeout: 5s
retries: 5
proxy:
image: nginx
volumes:
- type: bind
source: ./proxy/nginx.conf
target: /etc/nginx/conf.d/default.conf
read_only: true
ports:
- 80:80
depends_on:
- backend
volumes:
db-data:
secrets:
db-password:
file: db/password.txt

View File

@ -0,0 +1,84 @@
name: Pull request
# PRs created by GitHub Actions don't kick off further actions (https://github.com/peter-evans/create-pull-request/blob/d57e551ebc1a16dee0b8c9ea6d24dba7627a6e35/docs/concepts-guidelines.md#triggering-further-workflow-runs).
# As a workaround, we mark automerge PRs that are created by GitHub actions as draft and trigger the GitHub actions by marking the PR as ready for review. We'd prefer not re-triggering testing on a normal user's PR in this case, but skipping them causes the checks to reset.
permissions:
contents: read
on:
pull_request:
types: [opened, reopened, synchronize, ready_for_review]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
tests:
name: Test
uses: swiftlang/github-workflows/.github/workflows/swift_package_test.yml@0.0.11
with:
enable_cross_pr_testing: true
linux_exclude_swift_versions: "[{\"swift_version\": \"5.9\"}, {\"swift_version\": \"5.10\"}]"
windows_exclude_swift_versions: "[{\"swift_version\": \"5.9\"}]"
soundness:
name: Soundness
uses: swiftlang/github-workflows/.github/workflows/soundness.yml@0.0.11
with:
license_header_check_project_name: "Swift.org"
api_breakage_check_allowlist_path: "api-breakages.txt"
compatibility_check:
# Format a list of real-world projects that enforce formatting using the latest swift-format version.
name: Check compatibility with real world projects
runs-on: ubuntu-latest
container:
image: swift:6.2
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
persist-credentials: false
- name: Mark the workspace as safe
# https://github.com/actions/checkout/issues/766
run: git config --global --add safe.directory ${GITHUB_WORKSPACE}
- name: Check out related PRs
run: |
apt-get update && apt-get install -y curl
curl -s https://raw.githubusercontent.com/swiftlang/github-workflows/refs/heads/main/.github/workflows/scripts/cross-pr-checkout.swift > /tmp/cross-pr-checkout.swift
swift /tmp/cross-pr-checkout.swift "${{ github.repository }}" "${{ github.event.number }}"
- name: Build
run: |
swift build --configuration release
- name: Add local swift-format to PATH
run: |
# Add the locally built version of swift-format to PATH so it gets picked up by check-swift-format.sh
echo "PATH=$(pwd)/.build/release:$PATH" >> "$GITHUB_ENV"
- name: Download check format script
run: |
curl https://raw.githubusercontent.com/swiftlang/github-workflows/refs/heads/main/.github/workflows/scripts/check-swift-format.sh > /tmp/check-swift-format.sh
chmod +x /tmp/check-swift-format.sh
- name: Format swift-format
run: |
cd "$GITHUB_WORKSPACE"
/tmp/check-swift-format.sh
- name: Checkout swift-syntax
uses: actions/checkout@v6
with:
repository: swiftlang/swift-syntax
persist-credentials: false
path: swift-syntax
- name: Format swift-syntax
run: |
cd "$GITHUB_WORKSPACE/swift-syntax"
/tmp/check-swift-format.sh
- name: Checkout sourcekit-lsp
uses: actions/checkout@v6
with:
repository: swiftlang/sourcekit-lsp
persist-credentials: false
path: sourcekit-lsp
- name: Format sourcekit-lsp
run: |
cd "$GITHUB_WORKSPACE/sourcekit-lsp"
/tmp/check-swift-format.sh

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,12 @@
---
apiVersion: v1
kind: Pod
metadata:
name: pods-simple-pod
spec:
containers:
- command:
- sleep
- "3600"
image: busybox
name: pods-simple-container

View File

@ -0,0 +1,18 @@
resources:
- wordpress
- mysql
patches:
- path: patch.yaml
namePrefix: demo-
vars:
- name: WORDPRESS_SERVICE
objref:
kind: Service
name: wordpress
apiVersion: v1
- name: MYSQL_SERVICE
objref:
kind: Service
name: mysql
apiVersion: v1

View File

@ -0,0 +1,119 @@
openapi: "3.0.0"
info:
version: 1.0.0
title: Swagger Petstore
license:
name: MIT
servers:
- url: http://petstore.swagger.io/v1
paths:
/pets:
get:
summary: List all pets
operationId: listPets
tags:
- pets
parameters:
- name: limit
in: query
description: How many items to return at one time (max 100)
required: false
schema:
type: integer
maximum: 100
format: int32
responses:
'200':
description: A paged array of pets
headers:
x-next:
description: A link to the next page of responses
schema:
type: string
content:
application/json:
schema:
$ref: "#/components/schemas/Pets"
default:
description: unexpected error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
post:
summary: Create a pet
operationId: createPets
tags:
- pets
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/Pet'
required: true
responses:
'201':
description: Null response
default:
description: unexpected error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/pets/{petId}:
get:
summary: Info for a specific pet
operationId: showPetById
tags:
- pets
parameters:
- name: petId
in: path
required: true
description: The id of the pet to retrieve
schema:
type: string
responses:
'200':
description: Expected response to a valid request
content:
application/json:
schema:
$ref: "#/components/schemas/Pet"
default:
description: unexpected error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
components:
schemas:
Pet:
type: object
required:
- id
- name
properties:
id:
type: integer
format: int64
name:
type: string
tag:
type: string
Pets:
type: array
maxItems: 100
items:
$ref: "#/components/schemas/Pet"
Error:
type: object
required:
- code
- message
properties:
code:
type: integer
format: int32
message:
type: string

View File

@ -0,0 +1,481 @@
# my global config
global:
scrape_interval: 15s
evaluation_interval: 30s
body_size_limit: 15MB
sample_limit: 1500
target_limit: 30
label_limit: 30
label_name_length_limit: 200
label_value_length_limit: 200
query_log_file: query.log
scrape_failure_log_file: fail.log
# scrape_timeout is set to the global default (10s).
external_labels:
monitor: codelab
foo: bar
runtime:
gogc: 42
rule_files:
- "first.rules"
- "my/*.rules"
remote_write:
- url: http://remote1/push
name: drop_expensive
write_relabel_configs:
- source_labels: [__name__]
regex: expensive.*
action: drop
oauth2:
client_id: "123"
client_secret: "456"
token_url: "http://remote1/auth"
tls_config:
cert_file: valid_cert_file
key_file: valid_key_file
- url: http://remote2/push
protobuf_message: io.prometheus.write.v2.Request
name: rw_tls
tls_config:
cert_file: valid_cert_file
key_file: valid_key_file
headers:
name: value
otlp:
promote_resource_attributes: ["k8s.cluster.name", "k8s.job.name", "k8s.namespace.name"]
remote_read:
- url: http://remote1/read
read_recent: true
name: default
enable_http2: false
- url: http://remote3/read
read_recent: false
name: read_special
required_matchers:
job: special
tls_config:
cert_file: valid_cert_file
key_file: valid_key_file
scrape_configs:
- job_name: prometheus
honor_labels: true
# scrape_interval is defined by the configured global (15s).
# scrape_timeout is defined by the global default (10s).
# metrics_path defaults to '/metrics'
# scheme defaults to 'http'.
fallback_scrape_protocol: PrometheusText0.0.4
scrape_failure_log_file: fail_prom.log
file_sd_configs:
- files:
- foo/*.slow.json
- foo/*.slow.yml
- single/file.yml
refresh_interval: 10m
- files:
- bar/*.yaml
static_configs:
- targets: ["localhost:9090", "localhost:9191"]
labels:
my: label
your: label
http_headers:
foo:
values: ["foobar"]
secrets: ["bar", "foo"]
files: ["valid_password_file"]
relabel_configs:
- source_labels: [job, __meta_dns_name]
regex: (.*)some-[regex]
target_label: job
replacement: foo-${1}
# action defaults to 'replace'
- source_labels: [abc]
target_label: cde
- replacement: static
target_label: abc
- regex:
replacement: static
target_label: abc
- source_labels: [foo]
target_label: abc
action: keepequal
- source_labels: [foo]
target_label: abc
action: dropequal
authorization:
credentials_file: valid_token_file
tls_config:
min_version: TLS10
- job_name: service-x
basic_auth:
username: admin_name
password: "multiline\nmysecret\ntest"
scrape_interval: 50s
scrape_timeout: 5s
scrape_protocols: ["PrometheusText0.0.4"]
body_size_limit: 10MB
sample_limit: 1000
target_limit: 35
label_limit: 35
label_name_length_limit: 210
label_value_length_limit: 210
metrics_path: /my_path
scheme: https
dns_sd_configs:
- refresh_interval: 15s
names:
- first.dns.address.domain.com
- second.dns.address.domain.com
- names:
- first.dns.address.domain.com
relabel_configs:
- source_labels: [job]
regex: (.*)some-[regex]
action: drop
- source_labels: [__address__]
modulus: 8
target_label: __tmp_hash
action: hashmod
- source_labels: [__tmp_hash]
regex: 1
action: keep
- action: labelmap
regex: 1
- action: labeldrop
regex: d
- action: labelkeep
regex: k
metric_relabel_configs:
- source_labels: [__name__]
regex: expensive_metric.*
action: drop
- job_name: service-y
consul_sd_configs:
- server: "localhost:1234"
token: mysecret
path_prefix: /consul
services: ["nginx", "cache", "mysql"]
health_filter: 'Service.Tags contains "canary"'
tags: ["canary", "v1"]
node_meta:
rack: "123"
allow_stale: true
scheme: https
tls_config:
ca_file: valid_ca_file
cert_file: valid_cert_file
key_file: valid_key_file
insecure_skip_verify: false
relabel_configs:
- source_labels: [__meta_sd_consul_tags]
separator: ","
regex: label:([^=]+)=([^,]+)
target_label: ${1}
replacement: ${2}
- job_name: service-z
tls_config:
cert_file: valid_cert_file
key_file: valid_key_file
authorization:
credentials: mysecret
- job_name: service-kubernetes
kubernetes_sd_configs:
- role: endpoints
api_server: "https://localhost:1234"
tls_config:
cert_file: valid_cert_file
key_file: valid_key_file
basic_auth:
username: "myusername"
password: "mysecret"
- job_name: service-kubernetes-namespaces
kubernetes_sd_configs:
- role: endpoints
api_server: "https://localhost:1234"
namespaces:
names:
- default
basic_auth:
username: "myusername"
password_file: valid_password_file
- job_name: service-kuma
kuma_sd_configs:
- server: http://kuma-control-plane.kuma-system.svc:5676
client_id: main-prometheus
- job_name: service-marathon
marathon_sd_configs:
- servers:
- "https://marathon.example.com:443"
auth_token: "mysecret"
tls_config:
cert_file: valid_cert_file
key_file: valid_key_file
- job_name: service-nomad
nomad_sd_configs:
- server: 'http://localhost:4646'
- job_name: service-ec2
ec2_sd_configs:
- region: us-east-1
access_key: access
secret_key: mysecret
profile: profile
filters:
- name: tag:environment
values:
- prod
- name: tag:service
values:
- web
- db
- job_name: service-lightsail
lightsail_sd_configs:
- region: us-east-1
access_key: access
secret_key: mysecret
profile: profile
- job_name: service-azure
azure_sd_configs:
- environment: AzurePublicCloud
authentication_method: OAuth
subscription_id: 11AAAA11-A11A-111A-A111-1111A1111A11
resource_group: my-resource-group
tenant_id: BBBB222B-B2B2-2B22-B222-2BB2222BB2B2
client_id: 333333CC-3C33-3333-CCC3-33C3CCCCC33C
client_secret: mysecret
port: 9100
- job_name: service-nerve
nerve_sd_configs:
- servers:
- localhost
paths:
- /monitoring
- job_name: 0123service-xxx
metrics_path: /metrics
static_configs:
- targets:
- localhost:9090
- job_name: badfederation
honor_timestamps: false
metrics_path: /federate
static_configs:
- targets:
- localhost:9090
- job_name: 測試
metrics_path: /metrics
static_configs:
- targets:
- localhost:9090
- job_name: httpsd
http_sd_configs:
- url: "http://example.com/prometheus"
- job_name: service-triton
triton_sd_configs:
- account: "testAccount"
dns_suffix: "triton.example.com"
endpoint: "triton.example.com"
port: 9163
refresh_interval: 1m
version: 1
tls_config:
cert_file: valid_cert_file
key_file: valid_key_file
- job_name: digitalocean-droplets
digitalocean_sd_configs:
- authorization:
credentials: abcdef
- job_name: docker
docker_sd_configs:
- host: unix:///var/run/docker.sock
- job_name: dockerswarm
dockerswarm_sd_configs:
- host: http://127.0.0.1:2375
role: nodes
- job_name: service-openstack
openstack_sd_configs:
- role: instance
region: RegionOne
port: 80
refresh_interval: 1m
tls_config:
ca_file: valid_ca_file
cert_file: valid_cert_file
key_file: valid_key_file
- job_name: service-puppetdb
puppetdb_sd_configs:
- url: https://puppetserver/
query: 'resources { type = "Package" and title = "httpd" }'
include_parameters: true
port: 80
refresh_interval: 1m
tls_config:
ca_file: valid_ca_file
cert_file: valid_cert_file
key_file: valid_key_file
- job_name: hetzner
scrape_native_histograms: true
relabel_configs:
- action: uppercase
source_labels: [instance]
target_label: instance
hetzner_sd_configs:
- role: hcloud
authorization:
credentials: abcdef
- role: robot
basic_auth:
username: abcdef
password: abcdef
- job_name: service-eureka
eureka_sd_configs:
- server: "http://eureka.example.com:8761/eureka"
- job_name: ovhcloud
ovhcloud_sd_configs:
- service: vps
endpoint: ovh-eu
application_key: testAppKey
application_secret: testAppSecret
consumer_key: testConsumerKey
refresh_interval: 1m
- service: dedicated_server
endpoint: ovh-eu
application_key: testAppKey
application_secret: testAppSecret
consumer_key: testConsumerKey
refresh_interval: 1m
- job_name: scaleway
scaleway_sd_configs:
- role: instance
project_id: 11111111-1111-1111-1111-111111111112
access_key: SCWXXXXXXXXXXXXXXXXX
secret_key: 11111111-1111-1111-1111-111111111111
- role: baremetal
project_id: 11111111-1111-1111-1111-111111111112
access_key: SCWXXXXXXXXXXXXXXXXX
secret_key: 11111111-1111-1111-1111-111111111111
- job_name: linode-instances
linode_sd_configs:
- authorization:
credentials: abcdef
- job_name: stackit-servers
stackit_sd_configs:
- project: 11111111-1111-1111-1111-111111111111
service_account_key: mysecret_sa_key
private_key: mysecret_private_key
authorization:
credentials: abcdef
- job_name: uyuni
uyuni_sd_configs:
- server: https://localhost:1234
username: gopher
password: hole
- job_name: ionos
ionos_sd_configs:
- datacenter_id: 8feda53f-15f0-447f-badf-ebe32dad2fc0
authorization:
credentials: abcdef
- job_name: vultr
vultr_sd_configs:
- authorization:
credentials: abcdef
- job_name: outscale
outscale_sd_configs:
- region: eu-west-2
access_key: A1B2C3D4E5F6G7H8I9J0
secret_key: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
alerting:
alertmanagers:
- scheme: https
static_configs:
- targets:
- "1.2.3.4:9093"
- "1.2.3.5:9093"
- "1.2.3.6:9093"
storage:
tsdb:
out_of_order_time_window: 30m
stale_series_compaction_threshold: 0.5
retention:
time: 1d
size: 1GB
percentage: 28
tracing:
endpoint: "localhost:4317"
client_type: "grpc"
headers:
foo: "bar"
timeout: 5s
compression: "gzip"
tls_config:
cert_file: valid_cert_file
key_file: valid_key_file
insecure_skip_verify: true

View File

@ -0,0 +1,427 @@
# Code generated by cluster-api's prowjob-gen. DO NOT EDIT.
presubmits:
kubernetes-sigs/cluster-api:
- name: pull-cluster-api-build-release-1-13
cluster: eks-prow-build-cluster
decorate: true
decoration_config:
timeout: 180m
path_alias: sigs.k8s.io/cluster-api
always_run: true
branches:
# The script this job runs is not in all branches.
- ^release-1.13$
spec:
containers:
- image: gcr.io/k8s-staging-test-infra/kubekins-e2e:v20260601-2cbf4bdb47-1.35
command:
- runner.sh
- ./scripts/ci-build.sh
resources:
requests:
cpu: 6000m
memory: 4Gi
limits:
cpu: 6000m
memory: 4Gi
annotations:
testgrid-dashboards: cluster-api-core-1.13
testgrid-tab-name: capi-pr-build-release-1-13
- name: pull-cluster-api-apidiff-release-1-13
cluster: eks-prow-build-cluster
decorate: true
decoration_config:
timeout: 180m
path_alias: sigs.k8s.io/cluster-api
optional: true
branches:
# The script this job runs is not in all branches.
- ^release-1.13$
run_if_changed: '^((api|bootstrap|cmd|config|controllers|controlplane|errors|exp|feature|hack|internal|scripts|test|util|webhooks|version)/|main\.go|go\.mod|go\.sum|Dockerfile|Makefile)'
spec:
containers:
- command:
- runner.sh
- ./scripts/ci-apidiff.sh
image: gcr.io/k8s-staging-test-infra/kubekins-e2e:v20260601-2cbf4bdb47-1.35
resources:
requests:
cpu: 6000m
memory: 2Gi
limits:
cpu: 6000m
memory: 2Gi
annotations:
testgrid-dashboards: cluster-api-core-1.13
testgrid-tab-name: capi-pr-apidiff-release-1-13
- name: pull-cluster-api-verify-release-1-13
cluster: eks-prow-build-cluster
decorate: true
decoration_config:
timeout: 180m
path_alias: sigs.k8s.io/cluster-api
always_run: true
labels:
preset-dind-enabled: "true"
branches:
# The script this job runs is not in all branches.
- ^release-1.13$
spec:
containers:
- image: gcr.io/k8s-staging-test-infra/kubekins-e2e:v20260601-2cbf4bdb47-1.35
command:
- "runner.sh"
- ./scripts/ci-verify.sh
resources:
requests:
cpu: 5000m
memory: 3Gi
limits:
cpu: 5000m
memory: 3Gi
securityContext:
privileged: true
annotations:
testgrid-dashboards: cluster-api-core-1.13
testgrid-tab-name: capi-pr-verify-release-1-13
- name: pull-cluster-api-test-release-1-13
cluster: eks-prow-build-cluster
decorate: true
decoration_config:
timeout: 180m
path_alias: sigs.k8s.io/cluster-api
branches:
# The script this job runs is not in all branches.
- ^release-1.13$
run_if_changed: '^((api|bootstrap|cmd|config|controllers|controlplane|errors|exp|feature|hack|internal|scripts|test|util|webhooks|version)/|main\.go|go\.mod|go\.sum|Dockerfile|Makefile)'
spec:
containers:
- image: gcr.io/k8s-staging-test-infra/kubekins-e2e:v20260601-2cbf4bdb47-1.35
args:
- runner.sh
- ./scripts/ci-test.sh
resources:
requests:
cpu: 7
memory: 8Gi
limits:
cpu: 7
memory: 8Gi
annotations:
testgrid-dashboards: cluster-api-core-1.13
testgrid-tab-name: capi-pr-test-release-1-13
- name: pull-cluster-api-test-mink8s-release-1-13
cluster: eks-prow-build-cluster
decorate: true
decoration_config:
timeout: 180m
path_alias: sigs.k8s.io/cluster-api
always_run: false
branches:
# The script this job runs is not in all branches.
- ^release-1.13$
spec:
containers:
- image: gcr.io/k8s-staging-test-infra/kubekins-e2e:v20260601-2cbf4bdb47-1.35
args:
- runner.sh
- ./scripts/ci-test.sh
env:
# This value determines the minimum Kubernetes
# supported version for Cluster API management cluster
# and can be found by referring to [Supported Kubernetes Version](https://cluster-api.sigs.k8s.io/reference/versions.html#supported-kubernetes-versions)
# docs (look for minimum supported k8s version for management cluster, i.e N-3).
#
# To check the latest available envtest in Kubebuilder for the minor version we determined above, please
# refer to https://github.com/kubernetes-sigs/kubebuilder/tree/tools-releases.
- name: KUBEBUILDER_ENVTEST_KUBERNETES_VERSION
value: "1.32.0"
resources:
requests:
cpu: 7
memory: 8Gi
limits:
cpu: 7
memory: 8Gi
annotations:
testgrid-dashboards: cluster-api-core-1.13
testgrid-tab-name: capi-pr-test-mink8s-release-1-13
- name: pull-cluster-api-e2e-mink8s-release-1-13
cluster: eks-prow-build-cluster
labels:
preset-dind-enabled: "true"
preset-kind-volume-mounts: "true"
extra_refs:
- org: kubernetes
repo: kubernetes
base_ref: master
path_alias: k8s.io/kubernetes
decorate: true
decoration_config:
timeout: 180m
path_alias: sigs.k8s.io/cluster-api
always_run: false
branches:
# The script this job runs is not in all branches.
- ^release-1.13$
spec:
containers:
- image: gcr.io/k8s-staging-test-infra/kubekins-e2e:v20260601-2cbf4bdb47-1.35
args:
- runner.sh
- ./scripts/ci-e2e.sh
env:
# enable IPV6 in bootstrap image
- name: "DOCKER_IN_DOCKER_IPV6_ENABLED"
value: "true"
- name: GINKGO_LABEL_FILTER
value: "!Conformance"
# Ensure required kind images get built.
- name: KIND_BUILD_IMAGES
value: "KUBERNETES_VERSION,KUBERNETES_VERSION_LATEST_CI,KUBERNETES_VERSION_UPGRADE_TO,KUBERNETES_VERSION_UPGRADE_FROM"
# This value determines the minimum Kubernetes
# supported version for Cluster API management cluster
# and can be found by referring to [Supported Kubernetes Version](https://cluster-api.sigs.k8s.io/reference/versions.html#supported-kubernetes-versions)
# docs (look for minimum supported k8s version for management cluster, i.e N-3).
# Please also make sure to refer a version where a kindest/node image exists
# for (see https://github.com/kubernetes-sigs/kind/releases/)
- name: KUBERNETES_VERSION_MANAGEMENT
value: "v1.32.8"
# we need privileged mode in order to do docker in docker
securityContext:
privileged: true
resources:
requests:
cpu: 3000m
memory: 8Gi
limits:
cpu: 3000m
memory: 8Gi
annotations:
testgrid-dashboards: cluster-api-core-1.13
testgrid-tab-name: capi-pr-e2e-mink8s-release-1-13
- name: pull-cluster-api-e2e-blocking-release-1-13
cluster: eks-prow-build-cluster
labels:
preset-dind-enabled: "true"
preset-kind-volume-mounts: "true"
extra_refs:
- org: kubernetes
repo: kubernetes
base_ref: master
path_alias: k8s.io/kubernetes
decorate: true
decoration_config:
timeout: 180m
branches:
# The script this job runs is not in all branches.
- ^release-1.13$
path_alias: sigs.k8s.io/cluster-api
run_if_changed: '^((api|bootstrap|cmd|config|controllers|controlplane|errors|exp|feature|hack|internal|scripts|test|util|webhooks|version)/|main\.go|go\.mod|go\.sum|Dockerfile|Makefile)'
spec:
containers:
- image: gcr.io/k8s-staging-test-infra/kubekins-e2e:v20260601-2cbf4bdb47-1.35
args:
- runner.sh
- "./scripts/ci-e2e.sh"
env:
- name: GINKGO_LABEL_FILTER
value: "PR-Blocking"
# Ensure required kind images get built.
- name: KIND_BUILD_IMAGES
value: "KUBERNETES_VERSION"
# we need privileged mode in order to do docker in docker
securityContext:
privileged: true
resources:
requests:
cpu: 3000m
memory: 8Gi
limits:
cpu: 3000m
memory: 8Gi
annotations:
testgrid-dashboards: cluster-api-core-1.13
testgrid-tab-name: capi-pr-e2e-blocking-release-1-13
- name: pull-cluster-api-e2e-release-1-13
cluster: eks-prow-build-cluster
labels:
preset-dind-enabled: "true"
preset-kind-volume-mounts: "true"
extra_refs:
- org: kubernetes
repo: kubernetes
base_ref: master
path_alias: k8s.io/kubernetes
decorate: true
decoration_config:
timeout: 180m
always_run: false
branches:
# The script this job runs is not in all branches.
- ^release-1.13$
path_alias: sigs.k8s.io/cluster-api
spec:
containers:
- image: gcr.io/k8s-staging-test-infra/kubekins-e2e:v20260601-2cbf4bdb47-1.35
args:
- runner.sh
- "./scripts/ci-e2e.sh"
env:
# enable IPV6 in bootstrap image
- name: "DOCKER_IN_DOCKER_IPV6_ENABLED"
value: "true"
- name: GINKGO_LABEL_FILTER
value: "!Conformance"
# Ensure required kind images get built.
- name: KIND_BUILD_IMAGES
value: "KUBERNETES_VERSION,KUBERNETES_VERSION_LATEST_CI,KUBERNETES_VERSION_UPGRADE_TO,KUBERNETES_VERSION_UPGRADE_FROM"
# we need privileged mode in order to do docker in docker
securityContext:
privileged: true
resources:
requests:
cpu: 3000m
memory: 8Gi
limits:
cpu: 3000m
memory: 8Gi
annotations:
testgrid-dashboards: cluster-api-core-1.13
testgrid-tab-name: capi-pr-e2e-release-1-13
- name: pull-cluster-api-e2e-upgrade-1-36-1-37-release-1-13
cluster: eks-prow-build-cluster
labels:
preset-dind-enabled: "true"
preset-kind-volume-mounts: "true"
decorate: true
decoration_config:
timeout: 180m
always_run: false
branches:
# The script this job runs is not in all branches.
- ^release-1.13$
path_alias: sigs.k8s.io/cluster-api
extra_refs:
- org: kubernetes
repo: kubernetes
base_ref: master
path_alias: k8s.io/kubernetes
spec:
containers:
- image: gcr.io/k8s-staging-test-infra/kubekins-e2e:v20260601-2cbf4bdb47-1.35
args:
- runner.sh
- "./scripts/ci-e2e.sh"
env:
- name: ALWAYS_BUILD_KIND_IMAGES
value: "true"
- name: KUBERNETES_VERSION_UPGRADE_FROM
value: "stable-1.36"
- name: KUBERNETES_VERSION_UPGRADE_TO
value: "ci/latest-1.37"
- name: ETCD_VERSION_UPGRADE_TO
value: "3.6.8-0"
- name: COREDNS_VERSION_UPGRADE_TO
value: "v1.14.2"
- name: GINKGO_LABEL_FILTER
value: "(Conformance && K8s-Upgrade)"
# we need privileged mode in order to do docker in docker
securityContext:
privileged: true
resources:
requests:
cpu: 6000m
memory: 6Gi
limits:
cpu: 6000m
memory: 6Gi
annotations:
testgrid-dashboards: cluster-api-core-1.13
testgrid-tab-name: capi-pr-e2e-release-1-13-1-36-1-37
- name: pull-cluster-api-e2e-conformance-release-1-13
cluster: eks-prow-build-cluster
labels:
preset-dind-enabled: "true"
preset-kind-volume-mounts: "true"
extra_refs:
- org: kubernetes
repo: kubernetes
base_ref: master
path_alias: k8s.io/kubernetes
decorate: true
decoration_config:
timeout: 180m
always_run: false
branches:
# The script this job runs is not in all branches.
- ^release-1.13$
path_alias: sigs.k8s.io/cluster-api
spec:
containers:
- image: gcr.io/k8s-staging-test-infra/kubekins-e2e:v20260601-2cbf4bdb47-1.35
args:
- runner.sh
- "./scripts/ci-e2e.sh"
env:
- name: GINKGO_LABEL_FILTER
value: "(Conformance && K8s-Install)"
# Ensure required kind images get built.
- name: KIND_BUILD_IMAGES
value: "KUBERNETES_VERSION"
# we need privileged mode in order to do docker in docker
securityContext:
privileged: true
resources:
requests:
cpu: 4000m
memory: 4Gi
limits:
cpu: 4000m
memory: 4Gi
annotations:
testgrid-dashboards: cluster-api-core-1.13
testgrid-tab-name: capi-pr-e2e-conformance-release-1-13
- name: pull-cluster-api-e2e-conformance-ci-latest-release-1-13
cluster: eks-prow-build-cluster
labels:
preset-dind-enabled: "true"
preset-kind-volume-mounts: "true"
extra_refs:
- org: kubernetes
repo: kubernetes
base_ref: master
path_alias: k8s.io/kubernetes
decorate: true
decoration_config:
timeout: 180m
always_run: false
branches:
# The script this job runs is not in all branches.
- ^release-1.13$
path_alias: sigs.k8s.io/cluster-api
spec:
containers:
- image: gcr.io/k8s-staging-test-infra/kubekins-e2e:v20260601-2cbf4bdb47-1.35
args:
- runner.sh
- "./scripts/ci-e2e.sh"
env:
- name: GINKGO_LABEL_FILTER
value: "(Conformance && K8s-Install-ci-latest)"
# Ensure required kind images get built.
- name: KIND_BUILD_IMAGES
value: "KUBERNETES_VERSION_LATEST_CI"
# we need privileged mode in order to do docker in docker
securityContext:
privileged: true
resources:
requests:
cpu: 4000m
memory: 4Gi
limits:
cpu: 4000m
memory: 4Gi
annotations:
testgrid-dashboards: cluster-api-core-1.13
testgrid-tab-name: capi-pr-e2e-conformance-ci-latest-release-1-13

View File

@ -0,0 +1,7 @@
title: First
title: Second
paths:
/users:
get: {}
/users:
post: {}

View File

@ -0,0 +1,3 @@
slug: missing-title
metadata:
draft: true

View File

@ -0,0 +1,4 @@
title: Legacy
deprecated: true
nested:
deprecated: true

View File

@ -0,0 +1,22 @@
@testable import PureYAML
import Testing
@Suite("Parsing Aliases")
struct ParsingAliasTests {
@Test("Resolves anchors and aliases through the event composer")
func test_anchorsAndAliases() throws {
let root = try requireMapping(PureYAML.parse(
"""
first: &item {name: Example}
second: *item
""",
))
#expect(root?["first"] == root?["second"])
guard case let .mapping(second)? = root?["second"] else {
recordIssue("expected aliased mapping")
return
}
#expect(second["name"] == .string("Example"))
}
}

View File

@ -0,0 +1,67 @@
@testable import PureYAML
import Testing
@Suite("Parsing Errors")
struct ParsingErrorTests {
@Test("Reports exact parser errors")
func test_errorReporting() {
expectParseError("", .emptyDocument)
expectParseError("# comment only", .emptyDocument)
expectParseError("\tkey: value", .tabIndentation(line: 1))
expectParseError("root:\n child: yes\n wrong: no", .unexpectedIndentation(line: 3))
expectParseError("- one\nkey: value", .mixedCollectionStyles(line: 2))
expectParseError("root:\n child: value\n dangling", .unexpectedToken(
expected: "mapping key",
actual: "scalar value=\"dangling\" style=plain",
line: 3,
column: 3,
))
expectParseError("name: \"open", .unterminatedQuotedString(line: 1))
expectParseError("name: 'open", .unterminatedQuotedString(line: 1))
expectParseError("second: *missing", .undefinedAlias(anchor: "missing", line: 1, column: 9))
expectParseError("%YAML 1.3\n---\nvalue: yes", .incompatibleYAMLDirective(line: 1))
expectParseError("value: !!int nope", .invalidTaggedScalar(
tag: "tag:yaml.org,2002:int",
value: "nope",
line: 1,
column: 8,
))
}
@Test("Parser errors describe returned failures")
func test_errorDescriptions() {
#expect(PureYAML.Parsing.ParseError.emptyDocument.description == "document is empty")
#expect(PureYAML.Parsing.ParseError.tabIndentation(line: 2).description == "tabs are not allowed for indentation at line 2")
#expect(PureYAML.Parsing.ParseError.unexpectedIndentation(line: 3).description == "unexpected indentation at line 3")
#expect(PureYAML.Parsing.ParseError.mixedCollectionStyles(line: 4).description == "mapping and sequence entries are mixed at line 4")
#expect(PureYAML.Parsing.ParseError.expectedMappingKey(line: 5).description == "expected a mapping key at line 5")
#expect(PureYAML.Parsing.ParseError.expectedAnchorName(line: 6).description == "expected an anchor name at line 6")
#expect(PureYAML.Parsing.ParseError.expectedAliasName(line: 7).description == "expected an alias name at line 7")
#expect(PureYAML.Parsing.ParseError.unterminatedTag(line: 8).description == "unterminated tag at line 8")
#expect(PureYAML.Parsing.ParseError.unterminatedQuotedString(line: 9).description == "unterminated quoted string at line 9")
#expect(PureYAML.Parsing.ParseError.incompatibleYAMLDirective(line: 10).description == "incompatible YAML directive at line 10")
#expect(PureYAML.Parsing.ParseError.unsupportedDirective(name: "%FOO", line: 10).description == "unsupported directive '%FOO' at line 10")
#expect(PureYAML.Parsing.ParseError.unsupportedMultiDocumentStream(line: 10).description == "multi-document streams are not supported at line 10")
#expect(PureYAML.Parsing.ParseError.expectedNode(line: 10, column: 2).description == "expected a YAML node at line 10, column 2")
#expect(PureYAML.Parsing.ParseError.expectedScalarKey(line: 11, column: 3).description == "expected a scalar mapping key at line 11, column 3")
#expect(PureYAML.Parsing.ParseError.undefinedAlias(anchor: "item", line: 12, column: 4).description == "undefined alias 'item' at line 12, column 4")
#expect(PureYAML.Parsing.ParseError.invalidTaggedScalar(
tag: "tag:yaml.org,2002:int",
value: "nope",
line: 13,
column: 5,
).description == "invalid scalar 'nope' for tag 'tag:yaml.org,2002:int' at line 13, column 5")
#expect(PureYAML.Parsing.ParseError.unexpectedEvent(
expected: "mapping key",
actual: "sequenceStart anchor=- tag=- style=flow @1:1@0",
line: 13,
column: 5,
).description == "expected mapping key at line 13, column 5, found sequenceStart anchor=- tag=- style=flow @1:1@0")
#expect(PureYAML.Parsing.ParseError.unexpectedToken(
expected: "stream end",
actual: "mappingKey",
line: 14,
column: 1,
).description == "expected stream end at line 14, column 1, found mappingKey")
}
}

View File

@ -0,0 +1,218 @@
@testable import PureYAML
import Testing
@Suite("Parsing Indentation Regressions")
struct ParsingIndentationRegressionTests {
@Test("Parses compact sequence item mappings without swallowing lower-column siblings")
func test_compactSequenceItemMappingsDoNotSwallowLowerColumnSiblings() throws {
let root = try requireMapping(PureYAML.parse(
"""
patches:
- path: patch.yaml
namePrefix: demo-
vars:
- name: WORDPRESS_SERVICE
objref:
kind: Service
""",
))
let patch = root?.sequence("patches")?.first?.mapping
let variable = root?.sequence("vars")?.first?.mapping
#expect(patch?["path"] == .string("patch.yaml"))
#expect(patch?["namePrefix"] == nil)
#expect(root?["namePrefix"] == .string("demo-"))
#expect(variable?["name"] == .string("WORDPRESS_SERVICE"))
#expect(variable?.mapping("objref")?["kind"] == .string("Service"))
#expect(root?["missing"] == nil)
}
@Test("Parses compact sequence item mappings after indentless sequence values")
func test_compactSequenceItemMappingsAfterIndentlessSequenceValues() throws {
let root = try requireMapping(PureYAML.parse(
"""
spec:
containers:
- command:
- runner.sh
- ./scripts/ci-apidiff.sh
image: gcr.io/k8s-staging-test-infra/kubekins-e2e
resources:
requests:
cpu: 6000m
""",
))
let container = root?.mapping("spec")?.sequence("containers")?.first?.mapping
#expect(container?.sequence("command") == [
.string("runner.sh"),
.string("./scripts/ci-apidiff.sh"),
])
#expect(container?["image"] == .string("gcr.io/k8s-staging-test-infra/kubekins-e2e"))
#expect(container?.mapping("resources")?.mapping("requests")?["cpu"] == .string("6000m"))
#expect(container?["missing"] == nil)
}
@Test("Parses lower-column mapping siblings after nested compact sequence item mappings")
func test_lowerColumnMappingSiblingsAfterNestedCompactSequenceItemMappings() throws {
let root = try requireMapping(PureYAML.parse(
"""
presubmits:
org/repo:
- name: first
spec:
containers:
- command:
- runner.sh
resources:
requests:
cpu: 6000m
limits:
memory: 2Gi
annotations:
testgrid-dashboards: core
- name: second
cluster: build
""",
))
let jobs = root?.mapping("presubmits")?.sequence("org/repo")
let first = jobs?.first?.mapping
let second = jobs?.dropFirst().first?.mapping
let container = first?.mapping("spec")?.sequence("containers")?.first?.mapping
#expect(container?.sequence("command") == [.string("runner.sh")])
#expect(container?.mapping("resources")?.mapping("requests")?["cpu"] == .string("6000m"))
#expect(container?.mapping("resources")?.mapping("limits")?["memory"] == .string("2Gi"))
#expect(first?.mapping("annotations")?["testgrid-dashboards"] == .string("core"))
#expect(second?["name"] == .string("second"))
#expect(second?["cluster"] == .string("build"))
#expect(first?["missing"] == nil)
}
@Test("Parses indentless sequence values followed by flow siblings")
func test_indentlessSequenceValuesFollowedByFlowSiblings() throws {
let root = try requireMapping(PureYAML.parse(
"""
approveSignerNames:
- issuers.cert-manager.io/*
- clusterissuers.cert-manager.io/*
extraArgs: []
""",
))
#expect(root?.sequence("approveSignerNames") == [
.string("issuers.cert-manager.io/*"),
.string("clusterissuers.cert-manager.io/*"),
])
#expect(root?.sequence("extraArgs") == [])
#expect(root?["missing"] == nil)
}
@Test("Parses nested block sequence values followed by mapping siblings")
func test_nestedBlockSequenceValuesFollowedByMappingSiblings() throws {
let root = try requireMapping(PureYAML.parse(
"""
sort_by:
- - 123
- asc
- - 456
- desc
group_by:
- 123
vertical_group_by:
- 456
""",
))
#expect(root?.sequence("sort_by") == [
.sequence([.int(123), .string("asc")]),
.sequence([.int(456), .string("desc")]),
])
#expect(root?.sequence("group_by") == [.int(123)])
#expect(root?.sequence("vertical_group_by") == [.int(456)])
#expect(root?["missing"] == nil)
}
@Test("Parses explicit block scalar indentation indicators")
func test_explicitBlockScalarIndentationIndicators() throws {
let root = try requireMapping(PureYAML.parse(
"""
example:
value: |2
MMM. .MMM
MMMMMMMMMMMMMMMMMMM
MMMMMMMMMMMMMMMMMMMMM | Avoid administrative distraction. |
next: done
""",
))
#expect(
root?.mapping("example")?["value"] ==
.string(" MMM. .MMM\n MMMMMMMMMMMMMMMMMMM\nMMMMMMMMMMMMMMMMMMMMM | Avoid administrative distraction. |\n"),
)
#expect(root?.mapping("example")?["next"] == .string("done"))
#expect(root?.mapping("example")?["missing"] == nil)
}
@Test("Parses sequence item mappings after nested values")
func test_sequenceItemMappingsAfterNestedValues() throws {
let root = try requireMapping(PureYAML.parse(
"""
containers:
- command:
- sleep
- "3600"
image: busybox
name: worker
""",
))
let containers = requireSequence(root?["containers"])
guard let firstContainer = containers?.first else {
recordIssue("expected first container")
return
}
let container = requireMapping(firstContainer)
#expect(container?["command"] == .sequence([
.string("sleep"),
.string("3600"),
]))
#expect(container?["image"] == .string("busybox"))
#expect(container?["name"] == .string("worker"))
}
@Test("Parses sequence item mapping siblings after deep nested mapping values")
func test_sequenceItemMappingSiblingsAfterDeepNestedMappingValues() throws {
let root = try requireMapping(PureYAML.parse(
"""
runs:
- tool:
driver:
name: CodeQL
rules:
- id: js/unused-local-variable
name: js/unused-local-variable
results:
- guid: 326aa09f
message:
text: Unused variable foo.
""",
))
let run = root?.sequence("runs")?.first?.mapping
let driver = run?.mapping("tool")?.mapping("driver")
let rule = driver?.sequence("rules")?.first?.mapping
let result = run?.sequence("results")?.first?.mapping
#expect(driver?["name"] == .string("CodeQL"))
#expect(rule?["id"] == .string("js/unused-local-variable"))
#expect(rule?["name"] == .string("js/unused-local-variable"))
#expect(result?["guid"] == .string("326aa09f"))
#expect(result?.mapping("message")?["text"] == .string("Unused variable foo."))
#expect(run?["missing"] == nil)
}
}

View File

@ -0,0 +1,199 @@
@testable import PureYAML
import Testing
@Suite("Parsing Multiline Scalars")
struct ParsingMultilineScalarTests {
@Test("Parses multiline plain scalars in mappings")
func test_multilinePlainScalarsInMappings() throws {
let root = try requireMapping(PureYAML.parse(
"""
columns:
- description: CreationTimestamp is a timestamp representing the server time when
this object was created. It is not guaranteed to be set in happens-before
order across separate operations.
jsonPath: .metadata.creationTimestamp
""",
))
let expectedDescription = [
"CreationTimestamp is a timestamp representing the server time when this object was created.",
"It is not guaranteed to be set in happens-before order across separate operations.",
].joined(separator: " ")
let column = root?.sequence("columns")?.first?.mapping
#expect(column?["description"] == .string(expectedDescription))
#expect(column?["jsonPath"] == .string(".metadata.creationTimestamp"))
#expect(column?["missing"] == nil)
}
@Test("Parses multiline plain scalars before the next sequence entry")
func test_multilinePlainScalarsBeforeNextSequenceEntry() throws {
let value = try PureYAML.parse(
"""
- name: code-quality
description: Insights into reliability, maintainability, and efficiency of your
codebase.
- name: codespaces
description: Endpoints to manage Codespaces using the REST API.
""",
)
let tags = requireSequence(value)
let first = tags?.first?.mapping
let second = tags?.dropFirst().first?.mapping
#expect(first?["name"] == .string("code-quality"))
#expect(first?["description"] == .string("Insights into reliability, maintainability, and efficiency of your codebase."))
#expect(second?["name"] == .string("codespaces"))
#expect(second?["description"] == .string("Endpoints to manage Codespaces using the REST API."))
#expect(first?["missing"] == nil)
}
@Test("Parses multiline plain scalar sequence items")
func test_multilinePlainScalarSequenceItems() throws {
let root = try requireMapping(PureYAML.parse(
"""
examples:
- Updated instructions to help the team with planning and coordination
tasks
next: done
""",
))
#expect(root?.sequence("examples") == [
.string("Updated instructions to help the team with planning and coordination tasks"),
])
#expect(root?["next"] == .string("done"))
#expect(root?["missing"] == nil)
}
@Test("Parses multiline plain scalar continuations that start with quotes")
func test_multilinePlainScalarContinuationsThatStartWithQuotes() throws {
let root = try requireMapping(PureYAML.parse(
"""
get:
description: Gets a team using the team's slug. To create the slug, GitHub replaces
special characters in the name string, changes all words to lowercase, and
replaces spaces with a `-` separator and adds the "ent:" prefix. For example,
"My TEam Näme" would become `ent:my-team-name`.
tags:
- enterprise-teams
""",
))
let expectedDescription = [
"Gets a team using the team's slug.",
"To create the slug, GitHub replaces special characters in the name string,",
"changes all words to lowercase, and replaces spaces with a `-` separator",
"and adds the \"ent:\" prefix. For example,",
"\"My TEam Näme\" would become `ent:my-team-name`.",
].joined(separator: " ")
let get = root?.mapping("get")
#expect(get?["description"] == .string(expectedDescription))
#expect(get?.sequence("tags") == [.string("enterprise-teams")])
#expect(get?["missing"] == nil)
}
@Test("Parses multiline plain scalar continuations that start with flow markers")
func test_multilinePlainScalarContinuationsThatStartWithFlowMarkers() throws {
let root = try requireMapping(PureYAML.parse(
"""
selected_repository_ids:
description: An array of repository ids that can access the organization
secret. You can manage the list of
selected repositories using the [List selected repositories for
an organization secret](https://docs.example.com/list),
[Set selected repositories for an organization secret](https://docs.example.com/set),
and [Remove selected repository from an organization secret](https://docs.example.com/remove)
endpoints.
items:
type: integer
""",
))
let expectedDescription = [
"An array of repository ids that can access the organization secret.",
"You can manage the list of selected repositories using the",
"[List selected repositories for an organization secret](https://docs.example.com/list),",
"[Set selected repositories for an organization secret](https://docs.example.com/set),",
"and [Remove selected repository from an organization secret](https://docs.example.com/remove) endpoints.",
].joined(separator: " ")
let selected = root?.mapping("selected_repository_ids")
#expect(selected?["description"] == .string(expectedDescription))
#expect(selected?.mapping("items")?["type"] == .string("integer"))
#expect(selected?["missing"] == nil)
}
@Test("Parses flow-marker continuations after previous sequence indentation")
func test_flowMarkerContinuationsAfterPreviousSequenceIndentation() throws {
let root = try requireMapping(PureYAML.parse(
"""
schema:
enum:
- all
- private
selected_repository_ids:
description: An array of repository ids that can access the organization
[Set selected repositories](https://docs.example.com/set),
and more.
items:
type: integer
""",
))
let selected = root?.mapping("schema")?.mapping("selected_repository_ids")
#expect(
selected?["description"] ==
.string("An array of repository ids that can access the organization [Set selected repositories](https://docs.example.com/set), and more."),
)
#expect(selected?.mapping("items")?["type"] == .string("integer"))
#expect(selected?["missing"] == nil)
#expect(root?.mapping("schema")?.sequence("enum") == [.string("all"), .string("private")])
}
@Test("Parses colon-style markup inside multiline plain scalars")
func test_colonStyleMarkupInsideMultilinePlainScalars() throws {
let root = try requireMapping(PureYAML.parse(
"""
label:
description: The name of the label. Emoji can be added to label
names, using either native emoji or colon-style markup. For example,
typing `:strawberry:` will render the emoji ![:strawberry:](https://example.com/1f353.png
":strawberry:"). For a full list of available emoji and codes.
color: f29513
""",
))
let expectedDescription = [
"The name of the label. Emoji can be added to label names,",
"using either native emoji or colon-style markup. For example,",
"typing `:strawberry:` will render the emoji",
"![:strawberry:](https://example.com/1f353.png \":strawberry:\").",
"For a full list of available emoji and codes.",
].joined(separator: " ")
#expect(root?.mapping("label")?["description"] == .string(expectedDescription))
#expect(root?.mapping("label")?["color"] == .string("f29513"))
#expect(root?.mapping("label")?["missing"] == nil)
}
@Test("Parses block scalar text that starts with YAML indicators")
func test_blockScalarTextThatStartsWithYAMLIndicators() throws {
let root = try requireMapping(PureYAML.parse(
"""
description: |
**Fine-grained access tokens**
* [GitHub App user access tokens](https://docs.github.com)
- dash list item stays text
""",
))
#expect(root?["description"] == .string(
"""
**Fine-grained access tokens**
* [GitHub App user access tokens](https://docs.github.com)
- dash list item stays text
""",
))
}
}

View File

@ -130,6 +130,89 @@ struct ParsingTests {
#expect(second["description"] == .string("Production"))
}
@Test("Parses indentless sequences as mapping values")
func test_indentlessSequencesAsMappingValues() throws {
let root = try requireMapping(PureYAML.parse(
"""
resources:
- wordpress
- mysql
patches:
- path: patch.yaml
""",
))
#expect(root?["resources"] == .sequence([
.string("wordpress"),
.string("mysql"),
]))
#expect(root?["patches"] == .sequence([
.mapping(.init([
.init(key: "path", value: .string("patch.yaml")),
])),
]))
#expect(root?["missing"] == nil)
}
@Test("Parses nested indentless sequence values followed by mapping siblings")
func test_nestedIndentlessSequenceValuesInsideMappingsFollowedBySiblings() throws {
let root = try requireMapping(PureYAML.parse(
"""
spec:
names:
categories:
- cert-manager
kind: Certificate
shortNames:
- cert
- certs
singular: certificate
""",
))
let names = root?.mapping("spec")?.mapping("names")
#expect(names?.sequence("categories") == [.string("cert-manager")])
#expect(names?["kind"] == .string("Certificate"))
#expect(names?.sequence("shortNames") == [.string("cert"), .string("certs")])
#expect(names?["singular"] == .string("certificate"))
#expect(names?["missing"] == nil)
}
@Test("Parses indentless sequence values inside sequence item mappings")
func test_indentlessSequenceValuesInsideSequenceItemMappings() throws {
let root = try requireMapping(PureYAML.parse(
"""
presubmits:
kubernetes-sigs/cluster-api:
- name: pull-cluster-api-build-release-1-13
branches:
- ^release-1.13$
spec:
containers:
- image: gcr.io/k8s-staging-test-infra/kubekins-e2e
command:
- runner.sh
- ./scripts/ci-build.sh
annotations:
testgrid-dashboards: cluster-api-core-1.13
""",
))
let jobs = root?.mapping("presubmits")?.sequence("kubernetes-sigs/cluster-api")
let job = jobs?.first?.mapping
let container = job?.mapping("spec")?.sequence("containers")?.first?.mapping
#expect(job?["name"] == .string("pull-cluster-api-build-release-1-13"))
#expect(job?.sequence("branches") == [.string("^release-1.13$")])
#expect(container?["image"] == .string("gcr.io/k8s-staging-test-infra/kubekins-e2e"))
#expect(container?.sequence("command") == [
.string("runner.sh"),
.string("./scripts/ci-build.sh"),
])
#expect(job?.mapping("annotations")?["testgrid-dashboards"] == .string("cluster-api-core-1.13"))
#expect(job?["missing"] == nil)
}
@Test("Parses top-level sequences")
func test_topLevelSequences() throws {
let value = try PureYAML.parse(
@ -189,83 +272,4 @@ struct ParsingTests {
#expect(root?["stripped"] == .string("one\ntwo"))
#expect(root?["folded"] == .string("one two\n"))
}
@Test("Resolves anchors and aliases through the event composer")
func test_anchorsAndAliases() throws {
let root = try requireMapping(PureYAML.parse(
"""
first: &item {name: Example}
second: *item
""",
))
#expect(root?["first"] == root?["second"])
guard case let .mapping(second)? = root?["second"] else {
recordIssue("expected aliased mapping")
return
}
#expect(second["name"] == .string("Example"))
}
@Test("Reports exact parser errors")
func test_errorReporting() {
expectParseError("", .emptyDocument)
expectParseError("# comment only", .emptyDocument)
expectParseError("\tkey: value", .tabIndentation(line: 1))
expectParseError("root:\n child: yes\n wrong: no", .unexpectedIndentation(line: 3))
expectParseError("- one\nkey: value", .mixedCollectionStyles(line: 2))
expectParseError("root:\n child: value\n dangling", .unexpectedToken(
expected: "mapping key",
actual: "scalar value=\"dangling\" style=plain",
line: 3,
column: 3,
))
expectParseError("name: \"open", .unterminatedQuotedString(line: 1))
expectParseError("name: 'open", .unterminatedQuotedString(line: 1))
expectParseError("second: *missing", .undefinedAlias(anchor: "missing", line: 1, column: 9))
expectParseError("%YAML 1.3\n---\nvalue: yes", .incompatibleYAMLDirective(line: 1))
expectParseError("value: !!int nope", .invalidTaggedScalar(
tag: "tag:yaml.org,2002:int",
value: "nope",
line: 1,
column: 8,
))
}
@Test("Parser errors describe returned failures")
func test_errorDescriptions() {
#expect(PureYAML.Parsing.ParseError.emptyDocument.description == "document is empty")
#expect(PureYAML.Parsing.ParseError.tabIndentation(line: 2).description == "tabs are not allowed for indentation at line 2")
#expect(PureYAML.Parsing.ParseError.unexpectedIndentation(line: 3).description == "unexpected indentation at line 3")
#expect(PureYAML.Parsing.ParseError.mixedCollectionStyles(line: 4).description == "mapping and sequence entries are mixed at line 4")
#expect(PureYAML.Parsing.ParseError.expectedMappingKey(line: 5).description == "expected a mapping key at line 5")
#expect(PureYAML.Parsing.ParseError.expectedAnchorName(line: 6).description == "expected an anchor name at line 6")
#expect(PureYAML.Parsing.ParseError.expectedAliasName(line: 7).description == "expected an alias name at line 7")
#expect(PureYAML.Parsing.ParseError.unterminatedTag(line: 8).description == "unterminated tag at line 8")
#expect(PureYAML.Parsing.ParseError.unterminatedQuotedString(line: 9).description == "unterminated quoted string at line 9")
#expect(PureYAML.Parsing.ParseError.incompatibleYAMLDirective(line: 10).description == "incompatible YAML directive at line 10")
#expect(PureYAML.Parsing.ParseError.unsupportedDirective(name: "%FOO", line: 10).description == "unsupported directive '%FOO' at line 10")
#expect(PureYAML.Parsing.ParseError.unsupportedMultiDocumentStream(line: 10).description == "multi-document streams are not supported at line 10")
#expect(PureYAML.Parsing.ParseError.expectedNode(line: 10, column: 2).description == "expected a YAML node at line 10, column 2")
#expect(PureYAML.Parsing.ParseError.expectedScalarKey(line: 11, column: 3).description == "expected a scalar mapping key at line 11, column 3")
#expect(PureYAML.Parsing.ParseError.undefinedAlias(anchor: "item", line: 12, column: 4).description == "undefined alias 'item' at line 12, column 4")
#expect(PureYAML.Parsing.ParseError.invalidTaggedScalar(
tag: "tag:yaml.org,2002:int",
value: "nope",
line: 13,
column: 5,
).description == "invalid scalar 'nope' for tag 'tag:yaml.org,2002:int' at line 13, column 5")
#expect(PureYAML.Parsing.ParseError.unexpectedEvent(
expected: "mapping key",
actual: "sequenceStart anchor=- tag=- style=flow @1:1@0",
line: 13,
column: 5,
).description == "expected mapping key at line 13, column 5, found sequenceStart anchor=- tag=- style=flow @1:1@0")
#expect(PureYAML.Parsing.ParseError.unexpectedToken(
expected: "stream end",
actual: "mappingKey",
line: 14,
column: 1,
).description == "expected stream end at line 14, column 1, found mappingKey")
}
}

View File

@ -0,0 +1,355 @@
import Foundation
@testable import PureYAML
import Testing
@Suite("Real YAML Fixtures")
struct RealYAMLFixtureTests {
@Test("Real YAML fixture files match their pinned manifest", arguments: RealYAMLFixture.allCases)
func test_realYAMLFixtureFilesMatchPinnedManifest(fixture: RealYAMLFixture) throws {
let source = try fixture.load()
#expect(source.utf8.count == fixture.byteCount)
#expect(source.count(where: { $0 == "\n" }) == fixture.lineCount)
for requiredSubstring in fixture.requiredSubstrings {
#expect(source.contains(requiredSubstring))
}
for forbiddenSubstring in fixture.forbiddenSubstrings {
#expect(!source.contains(forbiddenSubstring))
}
}
@Test("Parses real YAML fixtures with exact representative values", arguments: RealYAMLFixture.allCases)
func test_parsesRealYAMLFixturesWithExactRepresentativeValues(fixture: RealYAMLFixture) throws {
let value = try PureYAML.parse(fixture.load())
fixture.expectRepresentativeValues(in: value)
#expect(PureYAML.Validation.Validator().collect(value) == PureYAML.Validation.Result())
}
}
enum RealYAMLFixture: String, CaseIterable, CustomStringConvertible {
case certManagerCertificateCRD = "cert-manager-certificate-crd.yaml"
case certManagerValues = "cert-manager-values.yaml"
case dockerComposeNginxGolangPostgres = "docker-compose-nginx-golang-postgres.yaml"
case githubActionsSwiftFormat = "github-actions-swift-format.yml"
case githubRestAPI = "github-rest-api.yaml"
case kubernetesSimplePod = "kubernetes-simple-pod.yaml"
case kustomizeWordPress = "kustomize-wordpress.yaml"
case openAPIPetstore = "openapi-petstore.yaml"
case prometheusGoodConfig = "prometheus-conf-good.yml"
case prowClusterAPIPresubmits = "prow-cluster-api-presubmits.yaml"
var description: String {
rawValue
}
var lineCount: Int {
switch self {
case .certManagerCertificateCRD:
883
case .certManagerValues:
1759
case .dockerComposeNginxGolangPostgres:
48
case .githubActionsSwiftFormat:
84
case .githubRestAPI:
257_280
case .kubernetesSimplePod:
12
case .kustomizeWordPress:
18
case .openAPIPetstore:
119
case .prometheusGoodConfig:
481
case .prowClusterAPIPresubmits:
427
}
}
var byteCount: Int {
switch self {
case .certManagerCertificateCRD:
44059
case .certManagerValues:
65536
case .dockerComposeNginxGolangPostgres:
852
case .githubActionsSwiftFormat:
3591
case .githubRestAPI:
9_549_118
case .kubernetesSimplePod:
186
case .kustomizeWordPress:
262
case .openAPIPetstore:
2768
case .prometheusGoodConfig:
11562
case .prowClusterAPIPresubmits:
14213
}
}
var requiredSubstrings: [String] {
switch self {
case .certManagerCertificateCRD:
[
"apiVersion: apiextensions.k8s.io/v1",
"jsonPath: .status.conditions[?(@.type == \"Ready\")].status",
"Known condition types are `Ready` and `Issuing`.",
]
case .certManagerValues:
[
"installCRDs: false",
"hostUsers: false",
"webhook:",
]
case .dockerComposeNginxGolangPostgres:
[
"services:",
"test: [ \"CMD\", \"pg_isready\" ]",
"file: db/password.txt",
]
case .githubActionsSwiftFormat:
[
"name: Pull request",
"types: [opened, reopened, synchronize, ready_for_review]",
"run: |",
]
case .githubRestAPI:
[
"openapi: 3.1.0",
"title: GitHub v3 REST API",
"components:",
]
case .kubernetesSimplePod:
[
"kind: Pod",
"name: pods-simple-container",
]
case .kustomizeWordPress:
[
"namePrefix: demo-",
"name: WORDPRESS_SERVICE",
]
case .openAPIPetstore:
[
"title: Swagger Petstore",
"operationId: listPets",
"'201':",
]
case .prometheusGoodConfig:
[
"scrape_interval: 15s",
"fallback_scrape_protocol: PrometheusText0.0.4",
"password: \"multiline\\nmysecret\\ntest\"",
]
case .prowClusterAPIPresubmits:
[
"presubmits:",
"run_if_changed: '^((api|bootstrap|cmd|config|controllers|controlplane|errors|exp|feature|hack|internal|scripts|test|util"
+ "|webhooks|version)/|main\\.go|go\\.mod|go\\.sum|Dockerfile|Makefile)'",
"securityContext:",
]
}
}
var forbiddenSubstrings: [String] {
[
"<<<<<<<",
"=======",
">>>>>>>",
]
}
func load() throws -> String {
let url = try #require(Bundle.module.url(
forResource: resourceName,
withExtension: resourceExtension,
subdirectory: "Fixtures/real-yaml",
))
return try String(contentsOf: url, encoding: .utf8)
}
func expectRepresentativeValues(in value: PureYAML.Model.Value) {
switch self {
case .certManagerCertificateCRD:
expectCertManagerCertificateCRD(value)
case .certManagerValues:
expectCertManagerValues(value)
case .dockerComposeNginxGolangPostgres:
expectDockerComposeNginxGolangPostgres(value)
case .githubActionsSwiftFormat:
expectGitHubActionsSwiftFormat(value)
case .githubRestAPI:
expectGitHubRestAPI(value)
case .kubernetesSimplePod:
expectKubernetesSimplePod(value)
case .kustomizeWordPress:
expectKustomizeWordPress(value)
case .openAPIPetstore:
expectOpenAPIPetstore(value)
case .prometheusGoodConfig:
expectPrometheusGoodConfig(value)
case .prowClusterAPIPresubmits:
expectProwClusterAPIPresubmits(value)
}
}
private var resourceName: String {
guard let dotIndex = rawValue.lastIndex(of: ".") else {
return rawValue
}
return String(rawValue[..<dotIndex])
}
private var resourceExtension: String {
guard let dotIndex = rawValue.lastIndex(of: ".") else {
return ""
}
return String(rawValue[rawValue.index(after: dotIndex)...])
}
}
private func expectCertManagerCertificateCRD(_ value: PureYAML.Model.Value) {
let root = requireMapping(value)
#expect(root?["apiVersion"] == .string("apiextensions.k8s.io/v1"))
#expect(root?["kind"] == .string("CustomResourceDefinition"))
#expect(root?.mapping("metadata")?["name"] == .string("certificates.cert-manager.io"))
let spec = root?.mapping("spec")
#expect(spec?.mapping("names")?["kind"] == .string("Certificate"))
let versions = requireSequence(spec?["versions"])
let firstVersion = versions?.first?.mapping
let firstColumn = firstVersion?.sequence("additionalPrinterColumns")?.first?.mapping
#expect(firstColumn?["name"] == .string("Ready"))
#expect(firstColumn?["jsonPath"] == .string(".status.conditions[?(@.type == \"Ready\")].status"))
}
private func expectCertManagerValues(_ value: PureYAML.Model.Value) {
let root = requireMapping(value)
#expect(root?.mapping("global")?.mapping("rbac")?["create"] == .bool(true))
#expect(root?["installCRDs"] == .bool(false))
#expect(root?.mapping("crds")?["enabled"] == .bool(false))
#expect(root?.mapping("crds")?["keep"] == .bool(true))
#expect(root?["replicaCount"] == .int(1))
#expect(root?.mapping("webhook")?["timeoutSeconds"] == .int(30))
}
private func expectDockerComposeNginxGolangPostgres(_ value: PureYAML.Model.Value) {
let root = requireMapping(value)
let services = root?.mapping("services")
#expect(services?.mapping("backend")?.mapping("build")?["context"] == .string("backend"))
#expect(services?.mapping("backend")?.mapping("depends_on")?.mapping("db")?["condition"] == .string("service_healthy"))
#expect(services?.mapping("db")?["image"] == .string("postgres"))
#expect(services?.mapping("db")?.sequence("healthcheck", "test") == [.string("CMD"), .string("pg_isready")])
#expect(services?.mapping("proxy")?.sequence("depends_on") == [.string("backend")])
#expect(root?.mapping("secrets")?.mapping("db-password")?["file"] == .string("db/password.txt"))
}
private func expectGitHubActionsSwiftFormat(_ value: PureYAML.Model.Value) {
let root = requireMapping(value)
#expect(root?["name"] == .string("Pull request"))
#expect(root?.mapping("permissions")?["contents"] == .string("read"))
#expect(root?.mapping("on")?.mapping("pull_request")?.sequence("types") == [
.string("opened"),
.string("reopened"),
.string("synchronize"),
.string("ready_for_review"),
])
#expect(root?.mapping("concurrency")?["cancel-in-progress"] == .bool(true))
#expect(
root?.mapping("jobs")?.mapping("tests")?["uses"] ==
.string("swiftlang/github-workflows/.github/workflows/swift_package_test.yml@0.0.11"),
)
#expect(root?.mapping("jobs")?.mapping("compatibility_check")?.mapping("container")?["image"] == .string("swift:6.2"))
}
private func expectGitHubRestAPI(_ value: PureYAML.Model.Value) {
let root = requireMapping(value)
#expect(root?["openapi"] == .string("3.1.0"))
#expect(root?.mapping("info")?["title"] == .string("GitHub v3 REST API"))
#expect(root?.mapping("info")?["version"] == .string("1.1.4"))
let firstTag = root?.sequence("tags")?.first?.mapping
#expect(firstTag?["name"] == .string("actions"))
#expect(root?.mapping("components")?.mapping("schemas")?.mapping("repository")?.mapping("properties")?.mapping("full_name")?["type"] == .string("string"))
}
private func expectKubernetesSimplePod(_ value: PureYAML.Model.Value) {
let root = requireMapping(value)
#expect(root?["apiVersion"] == .string("v1"))
#expect(root?["kind"] == .string("Pod"))
#expect(root?.mapping("metadata")?["name"] == .string("pods-simple-pod"))
let container = root?.mapping("spec")?.sequence("containers")?.first?.mapping
#expect(container?["image"] == .string("busybox"))
#expect(container?["name"] == .string("pods-simple-container"))
#expect(container?.sequence("command") == [.string("sleep"), .string("3600")])
}
private func expectKustomizeWordPress(_ value: PureYAML.Model.Value) {
let root = requireMapping(value)
#expect(root?.sequence("resources") == [.string("wordpress"), .string("mysql")])
#expect(root?.sequence("patches")?.first?.mapping?["path"] == .string("patch.yaml"))
#expect(root?["namePrefix"] == .string("demo-"))
let firstVariable = root?.sequence("vars")?.first?.mapping
#expect(firstVariable?["name"] == .string("WORDPRESS_SERVICE"))
#expect(firstVariable?.mapping("objref")?["kind"] == .string("Service"))
#expect(firstVariable?.mapping("objref")?["name"] == .string("wordpress"))
}
private func expectOpenAPIPetstore(_ value: PureYAML.Model.Value) {
let root = requireMapping(value)
#expect(root?["openapi"] == .string("3.0.0"))
#expect(root?.mapping("info")?["title"] == .string("Swagger Petstore"))
#expect(root?.mapping("paths")?.mapping("/pets")?.mapping("get")?["operationId"] == .string("listPets"))
#expect(root?.mapping("paths")?.mapping("/pets")?.mapping("post")?.mapping("responses")?["201"] != nil)
#expect(root?.mapping("components")?.mapping("schemas")?.mapping("Pet")?.sequence("required") == [
.string("id"),
.string("name"),
])
}
private func expectPrometheusGoodConfig(_ value: PureYAML.Model.Value) {
let root = requireMapping(value)
#expect(root?.mapping("global")?["scrape_interval"] == .string("15s"))
#expect(root?.mapping("global")?.mapping("external_labels")?["monitor"] == .string("codelab"))
#expect(root?.sequence("remote_write")?.first?.mapping?.mapping("oauth2")?["client_id"] == .string("123"))
let staticTargets = root?
.sequence("scrape_configs")?.first?
.mapping?
.sequence("static_configs")?.first?
.mapping?
.sequence("targets")
#expect(staticTargets == [.string("localhost:9090"), .string("localhost:9191")])
}
private func expectProwClusterAPIPresubmits(_ value: PureYAML.Model.Value) {
let root = requireMapping(value)
let jobs = root?.mapping("presubmits")?.sequence("kubernetes-sigs/cluster-api")
let firstJob = jobs?.first?.mapping
#expect(firstJob?["name"] == .string("pull-cluster-api-build-release-1-13"))
#expect(firstJob?["cluster"] == .string("eks-prow-build-cluster"))
#expect(firstJob?["always_run"] == .bool(true))
#expect(firstJob?.sequence("branches") == [.string("^release-1.13$")])
#expect(firstJob?.mapping("spec")?.sequence("containers")?.first?.mapping?["image"] == .string("gcr.io/k8s-staging-test-infra/kubekins-e2e:v20260601-2cbf4bdb47-1.35"))
}

View File

@ -31,6 +31,35 @@ func requireSequence(
return values
}
extension PureYAML.Model.Value {
var mapping: PureYAML.Model.Mapping? {
guard case let .mapping(mapping) = self else {
return nil
}
return mapping
}
}
extension PureYAML.Model.Mapping {
func mapping(_ key: String) -> PureYAML.Model.Mapping? {
self[key]?.mapping
}
func sequence(_ keys: String...) -> [PureYAML.Model.Value]? {
var value: PureYAML.Model.Value?
for key in keys {
let mapping: PureYAML.Model.Mapping? = if let currentValue = value {
currentValue.mapping
} else {
self
}
value = mapping?[key]
}
return requireSequence(value)
}
}
func expectParseError(
_ yaml: String,
_ expected: PureYAML.Parsing.ParseError,

View File

@ -0,0 +1,134 @@
import Foundation
@testable import PureYAML
import Testing
@Suite("Validation Failure Fixtures")
struct ValidationFailureFixtureTests {
@Test("Deliberately failing YAML fixtures report exact validation descriptions", arguments: validationFailureFixtures)
func test_deliberatelyFailingFixturesReportExactValidationDescriptions(testCase: ValidationFailureFixture) throws {
let value = try testCase.loadValue()
let result = testCase.validator.collect(value)
#expect(result.issues == testCase.expectedIssues)
#expect(result.issues.map(\.description) == testCase.expectedDescriptions)
#expect(result.descriptionLines == testCase.expectedDescriptions)
#expect(!result.isValid == testCase.expectedIssues.contains { $0.severity == .error })
expectValidationError(value, using: testCase.validator) { collection in
#expect(collection.issues == testCase.expectedIssues)
#expect(collection.description == testCase.expectedDescriptions.joined(separator: "\n"))
}
}
@Test("Warning-only failure fixture returns warnings in non-strict mode")
func test_warningOnlyFailureFixtureReturnsWarningsInNonStrictMode() throws {
let testCase = try #require(validationFailureFixtures.first { $0.name == "warning-only validation output" })
let value = try testCase.loadValue()
let warnings = try testCase.validator.validate(value, strict: false)
#expect(warnings == testCase.expectedIssues)
#expect(warnings.map(\.description) == testCase.expectedDescriptions)
#expect(warnings.allSatisfy { $0.severity == .warning })
#expect(!warnings.contains(.init(
severity: .warning,
reason: "Forbidden key 'deprecated' is present",
path: .init([.key("title")]),
)))
}
}
let validationFailureFixtures: [ValidationFailureFixture] = [
.init(
name: "duplicate key validation output",
fileName: "duplicate-keys",
validator: .init(),
expectedIssues: [
.init(
severity: .error,
reason: "Duplicate mapping key 'title'",
path: .init([.key("title")]),
),
.init(
severity: .error,
reason: "Duplicate mapping key '/users'",
path: .init([.key("paths"), .key("/users")]),
),
],
expectedDescriptions: [
"error: Duplicate mapping key 'title' at $.title",
"error: Duplicate mapping key '/users' at $.paths[\"/users\"]",
],
),
.init(
name: "required and forbidden validation output",
fileName: "missing-required-and-forbidden",
validator: .blank
.validating(requiredRootStringKeyRule("title"))
.validating(forbiddenMappingKeyRule("draft", severity: .error)),
expectedIssues: [
.init(
severity: .error,
reason: "Required string key 'title' is missing",
path: .init([.key("title")]),
),
.init(
severity: .error,
reason: "Forbidden key 'draft' is present",
path: .init([.key("metadata"), .key("draft")]),
),
],
expectedDescriptions: [
"error: Required string key 'title' is missing at $.title",
"error: Forbidden key 'draft' is present at $.metadata.draft",
],
),
.init(
name: "warning-only validation output",
fileName: "warnings",
validator: .blank.validating(forbiddenMappingKeyRule("deprecated", severity: .warning)),
expectedIssues: [
.init(
severity: .warning,
reason: "Forbidden key 'deprecated' is present",
path: .init([.key("deprecated")]),
),
.init(
severity: .warning,
reason: "Forbidden key 'deprecated' is present",
path: .init([.key("nested"), .key("deprecated")]),
),
],
expectedDescriptions: [
"warning: Forbidden key 'deprecated' is present at $.deprecated",
"warning: Forbidden key 'deprecated' is present at $.nested.deprecated",
],
),
]
struct ValidationFailureFixture: CustomStringConvertible {
var name: String
var fileName: String
var validator: PureYAML.Validation.Validator
var expectedIssues: [PureYAML.Validation.Issue]
var expectedDescriptions: [String]
var description: String {
name
}
func loadValue() throws -> PureYAML.Model.Value {
let url = try #require(Bundle.module.url(
forResource: fileName,
withExtension: "yaml",
subdirectory: "Fixtures/validation-failures",
))
let yaml = try String(contentsOf: url, encoding: .utf8)
return try PureYAML.parse(yaml)
}
}
private extension PureYAML.Validation.Result {
var descriptionLines: [String] {
issues.map(\.description)
}
}

View File

@ -22,6 +22,24 @@ validation rule must make these parts explicit:
- **Default status**: state whether the rule belongs in
`Validation.Validator.defaultRules` or must be opt-in.
## Validation Document Rule
User-facing tools must offer a validation document/report instead of exposing
only a raw crash, trap, stack trace, or unstructured thrown error. Library APIs
may throw typed errors, but CLIs, batch tools, and corpus runners must be able
to write or print a readable validation document that includes:
- input URL or file path;
- failure category;
- line and column when available;
- failing reference or path when applicable;
- original error text;
- whether processing continued or stopped.
Input-level failures must be data. A tool may return a non-zero exit code after
reporting failures, but it must not silently drop them or terminate before the
validation document can be produced.
## Test Contract
Every validator change must include Swift Testing coverage for:

View File

@ -66,12 +66,18 @@ source_singletons=$(
)
report_matches "Sources must not introduce singleton or service-locator access patterns" "$source_singletons"
source_traps=$(
grep -RInE '\bfatalError[[:space:]]*\(|\bpreconditionFailure[[:space:]]*\(|\bassertionFailure[[:space:]]*\(|\btry!|\bas!|[A-Za-z0-9_)\]}][[:space:]]*![[:space:]]*([).,\];:]|$)' Sources --include '*.swift' 2>/dev/null || true
)
report_matches "Sources must not use traps, forced casts, try!, or force unwraps" "$source_traps"
test_imports=$(
grep -RInE '^[[:space:]]*(@testable[[:space:]]+)?import[[:space:]]+[A-Za-z_][A-Za-z0-9_]*' Tests --include '*.swift' 2>/dev/null \
| grep -vE '^[^:]+:[0-9]+:[[:space:]]*@testable[[:space:]]+import[[:space:]]+PureYAML[[:space:]]*$' \
| grep -vE '^[^:]+:[0-9]+:[[:space:]]*import[[:space:]]+Foundation[[:space:]]*$' \
| grep -vE '^[^:]+:[0-9]+:[[:space:]]*import[[:space:]]+Testing[[:space:]]*$' || true
)
report_matches "Tests may import only Testing and @testable PureYAML" "$test_imports"
report_matches "Tests may import only Foundation, Testing, and @testable PureYAML" "$test_imports"
test_xctest=$(
grep -RInE '\bXCTest(Case|Expectation)?\b|\bXCTAssert[A-Za-z]*\b|^[[:space:]]*import[[:space:]]+XCTest[[:space:]]*$' Tests --include '*.swift' 2>/dev/null || true